Merge master into nao_ros2 (Redesign & Fixes)

This commit is contained in:
2026-02-01 22:33:20 -05:00
8 changed files with 985 additions and 1144 deletions

View File

@@ -1,6 +1,6 @@
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import { eq, sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import postgres from "postgres"; import postgres from "postgres";
import * as schema from "../src/server/db/schema"; import * as schema from "../src/server/db/schema";
@@ -9,49 +9,19 @@ const connectionString = process.env.DATABASE_URL!;
const connection = postgres(connectionString); const connection = postgres(connectionString);
const db = drizzle(connection, { schema }); const db = drizzle(connection, { schema });
// --- NAO6 Plugin Definitions (Inlined for reliability) --- // --- NAO6 Plugin Definitions (Synced from seed-nao6-plugin.ts) ---
const NAO_PLUGIN_DEF = { const NAO_PLUGIN_DEF = {
name: "NAO6 Robot (Enhanced ROS2 Integration)", name: "NAO6 Robot (Enhanced ROS2 Integration)",
version: "2.0.0", version: "2.0.0",
description: "Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2.", description: "Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2.",
actions: [ 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_speak", { 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 } } } },
name: "Speak Text", { 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 } } } },
category: "speech", { id: "nao_nod", name: "Nod Head", category: "interaction", parametersSchema: { type: "object", properties: { speed: { type: "number", default: 1.0 } } } },
parametersSchema: { { id: "nao_shake_head", name: "Shake Head", category: "interaction", parametersSchema: { type: "object", properties: { speed: { type: "number", default: 1.0 } } } },
type: "object", { id: "nao_bow", name: "Bow", category: "interaction", parametersSchema: { type: "object", properties: {} } },
properties: { { id: "nao_open_hand", name: "Present (Open Hand)", category: "interaction", parametersSchema: { type: "object", properties: { hand: { type: "string", enum: ["left", "right", "both"], default: "right" } } } }
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 }
}
}
}
] ]
}; };
@@ -59,9 +29,8 @@ async function main() {
console.log("🌱 Starting realistic seed script..."); console.log("🌱 Starting realistic seed script...");
try { try {
// 1. Clean existing data // 1. Clean existing data (Full Wipe)
console.log("🧹 Cleaning existing data..."); console.log("🧹 Cleaning existing data...");
// Delete in reverse dependency order
await db.delete(schema.mediaCaptures).where(sql`1=1`); await db.delete(schema.mediaCaptures).where(sql`1=1`);
await db.delete(schema.trialEvents).where(sql`1=1`); await db.delete(schema.trialEvents).where(sql`1=1`);
await db.delete(schema.trials).where(sql`1=1`); await db.delete(schema.trials).where(sql`1=1`);
@@ -87,7 +56,7 @@ async function main() {
email: "sean@soconnor.dev", email: "sean@soconnor.dev",
password: hashedPassword, password: hashedPassword,
emailVerified: new Date(), 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(); }).returning();
const [researcherUser] = await db.insert(schema.users).values({ 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"); if (!adminUser) throw new Error("Failed to create admin user");
await db.insert(schema.userSystemRoles).values({ await db.insert(schema.userSystemRoles).values({ userId: adminUser.id, role: "administrator" });
userId: adminUser.id,
role: "administrator",
});
// 3. Create Robots & Plugins // 3. Create Robots & Plugins
console.log("🤖 Creating robots and plugins..."); console.log("🤖 Creating robots and plugins...");
@@ -121,13 +87,13 @@ async function main() {
url: "https://github.com/hristudio/plugins", url: "https://github.com/hristudio/plugins",
description: "Official verified plugins", description: "Official verified plugins",
trustLevel: "official", trustLevel: "official",
status: "active", isEnabled: true,
isOfficial: true,
createdBy: adminUser.id, createdBy: adminUser.id,
}).returning(); }).returning();
const [naoPlugin] = await db.insert(schema.plugins).values({ const [naoPlugin] = await db.insert(schema.plugins).values({
robotId: naoRobot!.id, robotId: naoRobot!.id,
repositoryId: naoRepo!.id,
name: NAO_PLUGIN_DEF.name, name: NAO_PLUGIN_DEF.name,
version: NAO_PLUGIN_DEF.version, version: NAO_PLUGIN_DEF.version,
description: NAO_PLUGIN_DEF.description, description: NAO_PLUGIN_DEF.description,
@@ -136,22 +102,17 @@ async function main() {
status: "active", status: "active",
repositoryUrl: naoRepo!.url, repositoryUrl: naoRepo!.url,
actionDefinitions: NAO_PLUGIN_DEF.actions, actionDefinitions: NAO_PLUGIN_DEF.actions,
configurationSchema: { configurationSchema: { type: "object", properties: { robotIp: { type: "string", default: "192.168.1.100" } } },
type: "object",
properties: {
robotIp: { type: "string", default: "192.168.1.100" }
}
},
metadata: { category: "robot_control" } metadata: { category: "robot_control" }
}).returning(); }).returning();
// 4. Create Study & Experiment // 4. Create Study & Experiment - Comparative WoZ Study
console.log("📚 Creating study and experiment..."); console.log("📚 Creating 'Comparative WoZ Study'...");
const [study] = await db.insert(schema.studies).values({ const [study] = await db.insert(schema.studies).values({
name: "Social Robot Attention Study", name: "Comparative WoZ Study",
description: "Investigating the effect of robot gaze on participant attention retention.", description: "Comparison of HRIStudio vs Choregraphe for The Interactive Storyteller scenario.",
institution: "Bucknell University", institution: "Bucknell University",
irbProtocol: "2024-HRI-055", irbProtocol: "2024-HRI-COMP",
status: "active", status: "active",
createdBy: adminUser.id, createdBy: adminUser.id,
}).returning(); }).returning();
@@ -170,147 +131,203 @@ async function main() {
const [experiment] = await db.insert(schema.experiments).values({ const [experiment] = await db.insert(schema.experiments).values({
studyId: study!.id, studyId: study!.id,
name: "Attention Gaze Protocol A", name: "The Interactive Storyteller",
description: "Condition A: Robot maintains eye contact.", description: "A storytelling scenario where the robot tells a story and asks questions to the participant.",
version: 1, version: 1,
status: "ready", // Correct enum value status: "ready",
robotId: naoRobot!.id,
createdBy: adminUser.id, createdBy: adminUser.id,
}).returning(); }).returning();
// 5. Participants // 5. Create Steps & Actions (The Interactive Storyteller Protocol)
console.log("👤 Creating participants..."); 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 = []; const participants = [];
for (let i = 1; i <= 5; i++) { for (let i = 1; i <= 20; i++) {
participants.push({ participants.push({
studyId: study!.id, studyId: study!.id,
participantCode: `P${100 + i}`, participantCode: `P${100 + i}`,
name: `Participant ${100 + i}`, name: `Participant ${100 + i}`,
consentGiven: true, consentGiven: true,
consentGivenAt: new Date(), 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(); 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("\n✅ Database seeded successfully!");
console.log(`Summary:`); console.log(`Summary:`);
console.log(`- 1 Admin User (sean@soconnor.dev)`); 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} Participants`);
console.log(`- ${insertedParticipants.length} Trials created (mixed status)`);
console.log(`- ~20 Events per trial`);
} catch (error) { } catch (error) {
console.error("❌ Seeding failed:", error); console.error("❌ Seeding failed:", error);

View File

@@ -112,11 +112,10 @@ async function seedNAO6Plugin() {
description: description:
"Official NAO6 robot plugins for ROS2-based Human-Robot Interaction experiments", "Official NAO6 robot plugins for ROS2-based Human-Robot Interaction experiments",
trustLevel: "official", trustLevel: "official",
isActive: true, isEnabled: true,
isPublic: true, isOfficial: true,
createdBy: userId, createdBy: userId,
status: "active", lastSyncAt: new Date(),
lastSyncedAt: new Date(),
metadata: { metadata: {
author: { author: {
name: "HRIStudio Team", name: "HRIStudio Team",
@@ -487,10 +486,83 @@ async function seedNAO6Plugin() {
serviceType: "naoqi_bridge_msgs/srv/GetRobotInfo", 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 = { const pluginData: InsertPlugin = {
repositoryId: repoId,
robotId: robotId, robotId: robotId,
name: "NAO6 Robot (Enhanced ROS2 Integration)", name: "NAO6 Robot (Enhanced ROS2 Integration)",
version: "2.0.0", version: "2.0.0",

View File

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

View File

@@ -6,14 +6,15 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from "~/components/ui/card"; } from "~/components/ui/card";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Logo } from "~/components/ui/logo";
export default function SignInPage() { export default function SignInPage() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
@@ -52,30 +53,35 @@ export default function SignInPage() {
}; };
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4"> <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4">
<div className="w-full max-w-md"> {/* Background Gradients */}
<div className="absolute top-0 left-1/2 -z-10 h-[500px] w-[1000px] -translate-x-1/2 rounded-full bg-primary/20 blur-3xl opacity-30 dark:opacity-20" />
<div className="absolute bottom-0 right-0 -z-10 h-[300px] w-[300px] rounded-full bg-violet-500/10 blur-3xl" />
<div className="w-full max-w-md animate-in fade-in zoom-in-95 duration-500">
{/* Header */} {/* Header */}
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<Link href="/" className="inline-block"> <Link href="/" className="inline-flex items-center justify-center transition-opacity hover:opacity-80">
<h1 className="text-3xl font-bold text-slate-900">HRIStudio</h1> <Logo iconSize="lg" showText={false} />
</Link> </Link>
<p className="mt-2 text-slate-600"> <h1 className="mt-6 text-2xl font-bold tracking-tight text-foreground">Welcome back</h1>
<p className="mt-2 text-sm text-muted-foreground">
Sign in to your research account Sign in to your research account
</p> </p>
</div> </div>
{/* Sign In Card */} {/* Sign In Card */}
<Card> <Card className="border-muted/40 bg-card/50 backdrop-blur-sm shadow-xl">
<CardHeader> <CardHeader>
<CardTitle>Welcome back</CardTitle> <CardTitle>Sign In</CardTitle>
<CardDescription> <CardDescription>
Enter your credentials to access your account Enter your credentials to access the platform
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{error && ( {error && (
<div className="rounded-md bg-red-50 p-3 text-sm text-red-700"> <div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive font-medium border border-destructive/20">
{error} {error}
</div> </div>
)} )}
@@ -85,48 +91,52 @@ export default function SignInPage() {
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="your.email@example.com" placeholder="name@example.com"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
disabled={isLoading} disabled={isLoading}
className="bg-background/50"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password">Password</Label> <div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link href="#" className="text-xs text-primary hover:underline">Forgot password?</Link>
</div>
<Input <Input
id="password" id="password"
type="password" type="password"
placeholder="Enter your password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
disabled={isLoading} disabled={isLoading}
className="bg-background/50"
/> />
</div> </div>
<Button type="submit" className="w-full" disabled={isLoading}> <Button type="submit" className="w-full" disabled={isLoading} size="lg">
{isLoading ? "Signing in..." : "Sign In"} {isLoading ? "Signing in..." : "Sign In"}
</Button> </Button>
</form> </form>
<div className="mt-6 text-center text-sm text-slate-600"> <div className="mt-6 text-center text-sm text-muted-foreground">
Don&apos;t have an account?{" "} Don&apos;t have an account?{" "}
<Link <Link
href="/auth/signup" href="/auth/signup"
className="font-medium text-blue-600 hover:text-blue-500" className="font-medium text-primary hover:text-primary/80"
> >
Sign up here Sign up
</Link> </Link>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Footer */} {/* Footer */}
<div className="mt-8 text-center text-xs text-slate-500"> <div className="mt-8 text-center text-xs text-muted-foreground">
<p> <p>
© 2024 HRIStudio. A platform for Human-Robot Interaction research. &copy; {new Date().getFullYear()} HRIStudio. All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -5,14 +5,15 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from "~/components/ui/card"; } from "~/components/ui/card";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Logo } from "~/components/ui/logo";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
export default function SignUpPage() { export default function SignUpPage() {
@@ -55,30 +56,35 @@ export default function SignUpPage() {
}; };
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4"> <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4">
<div className="w-full max-w-md"> {/* Background Gradients */}
<div className="absolute top-0 left-1/2 -z-10 h-[500px] w-[1000px] -translate-x-1/2 rounded-full bg-primary/20 blur-3xl opacity-30 dark:opacity-20" />
<div className="absolute bottom-0 left-0 -z-10 h-[300px] w-[300px] rounded-full bg-blue-500/10 blur-3xl" />
<div className="w-full max-w-md animate-in fade-in zoom-in-95 duration-500">
{/* Header */} {/* Header */}
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<Link href="/" className="inline-block"> <Link href="/" className="inline-flex items-center justify-center transition-opacity hover:opacity-80">
<h1 className="text-3xl font-bold text-slate-900">HRIStudio</h1> <Logo iconSize="lg" showText={false} />
</Link> </Link>
<p className="mt-2 text-slate-600"> <h1 className="mt-6 text-2xl font-bold tracking-tight text-foreground">Create an account</h1>
Create your research account <p className="mt-2 text-sm text-muted-foreground">
Start your journey in HRI research
</p> </p>
</div> </div>
{/* Sign Up Card */} {/* Sign Up Card */}
<Card> <Card className="border-muted/40 bg-card/50 backdrop-blur-sm shadow-xl">
<CardHeader> <CardHeader>
<CardTitle>Get started</CardTitle> <CardTitle>Sign Up</CardTitle>
<CardDescription> <CardDescription>
Create your account to begin your HRI research Enter your details to create your research account
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{error && ( {error && (
<div className="rounded-md bg-red-50 p-3 text-sm text-red-700"> <div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive font-medium border border-destructive/20">
{error} {error}
</div> </div>
)} )}
@@ -88,11 +94,12 @@ export default function SignUpPage() {
<Input <Input
id="name" id="name"
type="text" type="text"
placeholder="Your full name" placeholder="John Doe"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
disabled={createUser.isPending} disabled={createUser.isPending}
className="bg-background/50"
/> />
</div> </div>
@@ -101,67 +108,73 @@ export default function SignUpPage() {
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="your.email@example.com" placeholder="name@example.com"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
disabled={createUser.isPending} disabled={createUser.isPending}
className="bg-background/50"
/> />
</div> </div>
<div className="space-y-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Label htmlFor="password">Password</Label> <div className="space-y-2">
<Input <Label htmlFor="password">Password</Label>
id="password" <Input
type="password" id="password"
placeholder="Create a password" type="password"
value={password} placeholder="******"
onChange={(e) => setPassword(e.target.value)} value={password}
required onChange={(e) => setPassword(e.target.value)}
disabled={createUser.isPending} required
minLength={6} disabled={createUser.isPending}
/> minLength={6}
</div> className="bg-background/50"
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label> <Label htmlFor="confirmPassword">Confirm</Label>
<Input <Input
id="confirmPassword" id="confirmPassword"
type="password" type="password"
placeholder="Confirm your password" placeholder="******"
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
disabled={createUser.isPending} disabled={createUser.isPending}
minLength={6} minLength={6}
/> className="bg-background/50"
/>
</div>
</div> </div>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
disabled={createUser.isPending} disabled={createUser.isPending}
size="lg"
> >
{createUser.isPending ? "Creating account..." : "Create Account"} {createUser.isPending ? "Creating account..." : "Create Account"}
</Button> </Button>
</form> </form>
<div className="mt-6 text-center text-sm text-slate-600"> <div className="mt-6 text-center text-sm text-muted-foreground">
Already have an account?{" "} Already have an account?{" "}
<Link <Link
href="/auth/signin" href="/auth/signin"
className="font-medium text-blue-600 hover:text-blue-500" className="font-medium text-primary hover:text-primary/80"
> >
Sign in here Sign in
</Link> </Link>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Footer */} {/* Footer */}
<div className="mt-8 text-center text-xs text-slate-500"> <div className="mt-8 text-center text-xs text-muted-foreground">
<p> <p>
© 2024 HRIStudio. A platform for Human-Robot Interaction research. &copy; {new Date().getFullYear()} HRIStudio. All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -2,17 +2,23 @@
import * as React from "react"; import * as React from "react";
import Link from "next/link"; import Link from "next/link";
import { import { format } from "date-fns";
Building,
FlaskConical,
TestTube,
Users,
Calendar,
Clock,
AlertCircle,
CheckCircle2,
} from "lucide-react";
import { formatDistanceToNow } 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 { Button } from "~/components/ui/button";
import { import {
@@ -22,7 +28,14 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "~/components/ui/card"; } 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 { Progress } from "~/components/ui/progress";
import { import {
Select, Select,
@@ -31,375 +44,270 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "~/components/ui/select"; } from "~/components/ui/select";
import { Badge } from "~/components/ui/badge";
import { ScrollArea } from "~/components/ui/scroll-area";
import { api } from "~/trpc/react"; 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 (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="bg-muted h-4 w-20 animate-pulse rounded" />
<div className="bg-muted h-8 w-8 animate-pulse rounded" />
</CardHeader>
<CardContent>
<div className="bg-muted h-8 w-12 animate-pulse rounded" />
<div className="bg-muted mt-2 h-3 w-24 animate-pulse rounded" />
</CardContent>
</Card>
))}
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{cards.map((card) => (
<Card key={card.title}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{card.title}</CardTitle>
<div className={`rounded-md p-2 ${card.bg}`}>
<card.icon className={`h-4 w-4 ${card.color}`} />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{card.value}</div>
<p className="text-muted-foreground text-xs">{card.description}</p>
</CardContent>
</Card>
))}
</div>
);
}
// 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 <CheckCircle2 className="h-4 w-4 text-green-600" />;
case "pending":
return <Clock className="h-4 w-4 text-yellow-600" />;
case "error":
return <AlertCircle className="h-4 w-4 text-red-600" />;
default:
return <AlertCircle className="h-4 w-4 text-blue-600" />;
}
};
return (
<Card className="col-span-4">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Latest updates from your research platform
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<div className="bg-muted h-4 w-4 animate-pulse rounded-full" />
<div className="flex-1 space-y-2">
<div className="bg-muted h-4 w-3/4 animate-pulse rounded" />
<div className="bg-muted h-3 w-1/2 animate-pulse rounded" />
</div>
<div className="bg-muted h-3 w-16 animate-pulse rounded" />
</div>
))}
</div>
) : activities.length === 0 ? (
<div className="py-8 text-center">
<AlertCircle className="text-muted-foreground mx-auto h-8 w-8" />
<p className="text-muted-foreground mt-2 text-sm">
No recent activity
</p>
</div>
) : (
<div className="space-y-4">
{activities.map((activity) => (
<div key={activity.id} className="flex items-center space-x-4">
{getStatusIcon(activity.status)}
<div className="flex-1 space-y-1">
<p className="text-sm leading-none font-medium">
{activity.title}
</p>
<p className="text-muted-foreground text-sm">
{activity.description}
</p>
</div>
<div className="text-muted-foreground text-sm">
{formatDistanceToNow(activity.time, { addSuffix: true })}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
// 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 (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{actions.map((action) => (
<Card
key={action.title}
className="group cursor-pointer transition-all hover:shadow-md"
>
<CardContent className="p-6">
<Button asChild className={`w-full ${action.color} text-white`}>
<Link href={action.href}>
<action.icon className="mr-2 h-4 w-4" />
{action.title}
</Link>
</Button>
<p className="text-muted-foreground mt-2 text-sm">
{action.description}
</p>
</CardContent>
</Card>
))}
</div>
);
}
// Study Progress Component
function StudyProgress({ studyFilter }: { studyFilter: string | null }) {
const { data: studies = [], isLoading } =
api.dashboard.getStudyProgress.useQuery({
limit: 5,
studyId: studyFilter ?? undefined,
});
return (
<Card className="col-span-3">
<CardHeader>
<CardTitle>Study Progress</CardTitle>
<CardDescription>
Current status of active research studies
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="bg-muted h-4 w-32 animate-pulse rounded" />
<div className="bg-muted h-3 w-24 animate-pulse rounded" />
</div>
<div className="bg-muted h-5 w-16 animate-pulse rounded" />
</div>
<div className="bg-muted h-2 w-full animate-pulse rounded" />
<div className="bg-muted h-3 w-16 animate-pulse rounded" />
</div>
))}
</div>
) : studies.length === 0 ? (
<div className="py-8 text-center">
<Building className="text-muted-foreground mx-auto h-8 w-8" />
<p className="text-muted-foreground mt-2 text-sm">
No active studies found
</p>
<p className="text-muted-foreground text-xs">
Create a study to get started
</p>
</div>
) : (
<div className="space-y-6">
{studies.map((study) => (
<div key={study.id} className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm leading-none font-medium">
{study.name}
</p>
<p className="text-muted-foreground text-sm">
{study.participants}/{study.totalParticipants} completed
trials
</p>
</div>
<Badge
variant={
study.status === "active" ? "default" : "secondary"
}
>
{study.status}
</Badge>
</div>
<Progress value={study.progress} className="h-2" />
<p className="text-muted-foreground text-xs">
{study.progress}% complete
</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
export default function DashboardPage() { export default function DashboardPage() {
const [studyFilter, setStudyFilter] = React.useState<string | null>(null); const [studyFilter, setStudyFilter] = React.useState<string | null>(null);
// Get user studies for filter dropdown // --- Data Fetching ---
const { data: userStudiesData } = api.studies.list.useQuery({ const { data: userStudiesData } = api.studies.list.useQuery({
memberOnly: true, memberOnly: true,
limit: 100, limit: 100,
}); });
const userStudies = userStudiesData?.studies ?? []; 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 ( return (
<div className="space-y-6"> <div className="flex flex-col space-y-8 animate-in fade-in duration-500">
{/* Header */} {/* Header Section */}
<div className="flex items-center justify-between"> <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight"> <h1 className="text-3xl font-bold tracking-tight text-foreground">Dashboard</h1>
Dashboard
{studyFilter && (
<Badge variant="secondary" className="ml-2">
{userStudies.find((s) => s.id === studyFilter)?.name}
</Badge>
)}
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{studyFilter Overview of your research activities and upcoming tasks.
? "Study-specific dashboard view"
: "Welcome to your HRI Studio research platform"}
</p> </p>
</div> </div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2"> <div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm"> <Select
Filter by study: value={studyFilter ?? "all"}
</span> onValueChange={(value) =>
<Select setStudyFilter(value === "all" ? null : value)
value={studyFilter ?? "all"} }
onValueChange={(value) => >
setStudyFilter(value === "all" ? null : value) <SelectTrigger className="w-[200px] bg-background">
} <SelectValue placeholder="All Studies" />
> </SelectTrigger>
<SelectTrigger className="w-[200px]"> <SelectContent>
<SelectValue placeholder="All Studies" /> <SelectItem value="all">All Studies</SelectItem>
</SelectTrigger> {userStudies.map((study) => (
<SelectContent> <SelectItem key={study.id} value={study.id}>
<SelectItem value="all">All Studies</SelectItem> {study.name}
{userStudies.map((study) => ( </SelectItem>
<SelectItem key={study.id} value={study.id}> ))}
{study.name} </SelectContent>
</SelectItem> </Select>
))}
</SelectContent> <Button asChild>
</Select> <Link href="/studies/new">
</div> <Plus className="mr-2 h-4 w-4" /> New Study
<Badge variant="outline" className="text-xs"> </Link>
<Calendar className="mr-1 h-3 w-3" /> </Button>
{new Date().toLocaleDateString()}
</Badge>
</div> </div>
</div> </div>
{/* Overview Cards */} {/* Stats Cards */}
<OverviewCards studyFilter={studyFilter} /> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatsCard
title="Total Participants"
value={stats?.totalParticipants ?? 0}
icon={Users}
description="Across all studies"
trend="+2 this week"
/>
<StatsCard
title="Active Trials"
value={stats?.activeTrials ?? 0}
icon={Activity}
description="Currently in progress"
{/* Main Content Grid */} />
<div className="grid gap-4 lg:grid-cols-7"> <StatsCard
<StudyProgress studyFilter={studyFilter} /> title="Completed Trials"
value={stats?.completedToday ?? 0}
icon={CheckCircle2}
description="Completed today"
/>
<StatsCard
title="Scheduled"
value={stats?.scheduledTrials ?? 0}
icon={Calendar}
description="Upcoming sessions"
/>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
{/* Main Column: Scheduled Trials & Study Progress */}
<div className="col-span-4 space-y-4"> <div className="col-span-4 space-y-4">
<RecentActivity studyFilter={studyFilter} />
</div>
</div>
{/* Quick Actions */} {/* Scheduled Trials */}
<div className="space-y-4"> <Card className="col-span-4 border-muted/40 shadow-sm">
<h2 className="text-xl font-semibold">Quick Actions</h2> <CardHeader>
<QuickActions /> <div className="flex items-center justify-between">
<div>
<CardTitle>Upcoming Sessions</CardTitle>
<CardDescription>
You have {scheduledTrials?.length ?? 0} scheduled trials coming up.
</CardDescription>
</div>
<Button variant="ghost" size="sm" asChild>
<Link href="/trials?status=scheduled">View All <ArrowRight className="ml-2 h-4 w-4" /></Link>
</Button>
</div>
</CardHeader>
<CardContent>
{!scheduledTrials?.length ? (
<div className="flex h-[150px] flex-col items-center justify-center rounded-md border border-dashed text-center animate-in fade-in-50">
<Calendar className="h-8 w-8 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">No scheduled trials found.</p>
<Button variant="link" size="sm" asChild className="mt-1">
<Link href="/trials/new">Schedule a Trial</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{scheduledTrials.map((trial) => (
<div key={trial.id} className="flex items-center justify-between rounded-lg border p-3 hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
<Calendar className="h-5 w-5" />
</div>
<div>
<p className="font-medium text-sm">
{trial.participant.participantCode}
<span className="ml-2 text-muted-foreground font-normal text-xs"> {trial.experiment.name}</span>
</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{trial.scheduledAt ? format(trial.scheduledAt, "MMM d, h:mm a") : "Unscheduled"}
</div>
</div>
</div>
<Button size="sm" className="gap-2" asChild>
<Link href={`/wizard/${trial.id}`}>
<Play className="h-3.5 w-3.5" /> Start
</Link>
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Study Progress */}
<Card className="border-muted/40 shadow-sm">
<CardHeader>
<CardTitle>Study Progress</CardTitle>
<CardDescription>
Completion tracking for active studies
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{studyProgress?.map((study) => (
<div key={study.id} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="font-medium">{study.name}</div>
<div className="text-muted-foreground">{study.participants} / {study.totalParticipants} Participants</div>
</div>
<Progress value={study.progress} className="h-2" />
</div>
))}
{!studyProgress?.length && (
<p className="text-sm text-muted-foreground text-center py-4">No active studies to track.</p>
)}
</CardContent>
</Card>
</div>
{/* Side Column: Recent Activity & Quick Actions */}
<div className="col-span-3 space-y-4">
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-4">
<Button variant="outline" className="h-24 flex-col gap-2 hover:border-primary/50 hover:bg-primary/5" asChild>
<Link href="/experiments/new">
<Bot className="h-6 w-6 mb-1" />
<span>New Experim.</span>
</Link>
</Button>
<Button variant="outline" className="h-24 flex-col gap-2 hover:border-primary/50 hover:bg-primary/5" asChild>
<Link href="/trials/new">
<PlayCircle className="h-6 w-6 mb-1" />
<span>Run Trial</span>
</Link>
</Button>
</div>
{/* Recent Activity */}
<Card className="border-muted/40 shadow-sm h-full">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-4">
{recentActivity?.map((activity) => (
<div key={activity.id} className="relative pl-4 pb-1 border-l last:border-0 border-muted-foreground/20">
<span className="absolute left-[-5px] top-1 h-2.5 w-2.5 rounded-full bg-primary/30 ring-4 ring-background" />
<div className="mb-1 text-sm font-medium leading-none">{activity.title}</div>
<div className="text-xs text-muted-foreground mb-1">{activity.description}</div>
<div className="text-[10px] text-muted-foreground/70 uppercase">
{formatDistanceToNow(activity.time, { addSuffix: true })}
</div>
</div>
))}
{!recentActivity?.length && (
<p className="text-sm text-muted-foreground text-center py-8">No recent activity.</p>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
</div> </div>
</div> </div>
); );
} }
function StatsCard({
title,
value,
icon: Icon,
description,
trend,
}: {
title: string;
value: string | number;
icon: React.ElementType;
description: string;
trend?: string;
}) {
return (
<Card className="border-muted/40 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">
{description}
{trend && <span className="ml-1 text-green-600 dark:text-green-400 font-medium">{trend}</span>}
</p>
</CardContent>
</Card>
);
}

View File

@@ -5,561 +5,290 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Logo } from "~/components/ui/logo"; import { Logo } from "~/components/ui/logo";
import { auth } from "~/server/auth"; import { auth } from "~/server/auth";
import {
ArrowRight,
Beaker,
Bot,
Database,
LayoutTemplate,
Lock,
Network,
PlayCircle,
Settings2,
Share2,
} from "lucide-react";
export default async function Home() { export default async function Home() {
const session = await auth(); const session = await auth();
// Redirect authenticated users to their dashboard
if (session?.user) { if (session?.user) {
redirect("/dashboard"); redirect("/dashboard");
} }
return ( return (
<main className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100"> <div className="flex min-h-screen flex-col bg-background text-foreground">
{/* Header */} {/* Navbar */}
<div className="border-b bg-white/50 backdrop-blur-sm"> <header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4"> <div className="container mx-auto flex h-16 items-center justify-between px-4">
<div className="flex items-center justify-between"> <Logo iconSize="md" showText={true} />
<Logo iconSize="md" showText={true} /> <nav className="flex items-center gap-4">
<Button variant="ghost" asChild className="hidden sm:inline-flex">
<Link href="#features">Features</Link>
</Button>
<Button variant="ghost" asChild className="hidden sm:inline-flex">
<Link href="#architecture">Architecture</Link>
</Button>
<div className="h-6 w-px bg-border hidden sm:block" />
<Button variant="ghost" asChild>
<Link href="/auth/signin">Sign In</Link>
</Button>
<Button asChild>
<Link href="/auth/signup">Get Started</Link>
</Button>
</nav>
</div>
</header>
<div className="flex items-center gap-4"> <main className="flex-1">
<Button asChild variant="outline"> {/* Hero Section */}
<Link href="/auth/signin">Sign In</Link> <section className="relative overflow-hidden pt-20 pb-32 md:pt-32">
{/* Background Gradients */}
<div className="absolute top-0 left-1/2 -z-10 h-[500px] w-[1000px] -translate-x-1/2 rounded-full bg-primary/20 blur-3xl opacity-30 dark:opacity-20" />
<div className="container mx-auto flex flex-col items-center px-4 text-center">
<Badge variant="secondary" className="mb-6 rounded-full px-4 py-1.5 text-sm font-medium">
The Modern Standard for HRI Research
</Badge>
<h1 className="max-w-4xl text-5xl font-extrabold tracking-tight sm:text-6xl md:text-7xl">
Reproducible WoZ Studies <br className="hidden md:block" />
<span className="bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent dark:from-blue-400 dark:to-violet-400">
Made Simple
</span>
</h1>
<p className="mt-6 max-w-2xl text-lg text-muted-foreground md:text-xl">
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.
</p>
<div className="mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center">
<Button size="lg" className="h-12 px-8 text-base" asChild>
<Link href="/auth/signup">
Start Researching
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button> </Button>
<Button asChild> <Button size="lg" variant="outline" className="h-12 px-8 text-base" asChild>
<Link href="/auth/signup">Get Started</Link> <Link href="https://github.com/robolab/hristudio" target="_blank">
View on GitHub
</Link>
</Button> </Button>
</div> </div>
</div>
</div>
</div>
{/* Hero Section */} {/* Mockup / Visual Interest */}
<section className="container mx-auto px-4 py-20"> <div className="relative mt-20 w-full max-w-5xl rounded-xl border bg-background/50 p-2 shadow-2xl backdrop-blur-sm lg:rounded-2xl lg:p-4">
<div className="mx-auto max-w-4xl text-center"> <div className="absolute inset-x-0 -top-px mx-auto h-px w-3/4 bg-gradient-to-r from-transparent via-foreground/20 to-transparent" />
<Badge variant="secondary" className="mb-4"> <div className="aspect-[16/9] w-full overflow-hidden rounded-lg border bg-muted/50 flex items-center justify-center relative">
🤖 Human-Robot Interaction Research Platform {/* Placeholder for actual app screenshot */}
</Badge> <div className="absolute inset-0 bg-gradient-to-tr from-blue-500/10 to-violet-500/10" />
<h1 className="mb-6 text-5xl font-bold tracking-tight text-slate-900"> <div className="text-center p-8">
Standardize Your <LayoutTemplate className="w-16 h-16 mx-auto text-muted-foreground/50 mb-4" />
<span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> <p className="text-muted-foreground font-medium">Interactive Experiment Designer</p>
{" "} </div>
Wizard of Oz{" "}
</span>
Studies
</h1>
<p className="mb-8 text-xl leading-relaxed text-slate-600">
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.
</p>
<div className="flex flex-col justify-center gap-4 sm:flex-row">
<Button size="lg" asChild>
<Link href="/auth/signup">Start Your Research</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="#features">Learn More</Link>
</Button>
</div>
</div>
</section>
{/* Problem Section */}
<section className="bg-white/50 py-20">
<div className="container mx-auto px-4">
<div className="mx-auto max-w-4xl">
<div className="mb-12 text-center">
<h2 className="mb-4 text-3xl font-bold text-slate-900">
The Challenge of WoZ Studies
</h2>
<p className="text-lg text-slate-600">
While Wizard of Oz is a powerful paradigm for HRI research, it
faces significant challenges
</p>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-red-600">
Reproducibility Issues
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-slate-600">
<li> Wizard behavior variability across trials</li>
<li> Inconsistent experimental conditions</li>
<li> Lack of standardized terminology</li>
<li> Insufficient documentation</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-red-600">
Technical Barriers
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-slate-600">
<li> Platform-specific robot control systems</li>
<li> Extensive custom coding requirements</li>
<li> Limited to domain experts</li>
<li> Fragmented data collection</li>
</ul>
</CardContent>
</Card>
</div>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="py-20">
<div className="container mx-auto px-4">
<div className="mx-auto max-w-6xl">
<div className="mb-16 text-center">
<h2 className="mb-4 text-3xl font-bold text-slate-900">
Six Key Design Principles
</h2>
<p className="text-lg text-slate-600">
Our platform addresses these challenges through comprehensive
design principles
</p>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
<Card className="border-blue-200 bg-blue-50/50">
<CardHeader>
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100">
<svg
className="h-6 w-6 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<CardTitle>Integrated Environment</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">
All functionalities unified in a single web-based platform
with intuitive interfaces
</p>
</CardContent>
</Card>
<Card className="border-green-200 bg-green-50/50">
<CardHeader>
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-green-100">
<svg
className="h-6 w-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<CardTitle>Visual Experiment Design</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">
Minimal-to-no coding required with drag-and-drop visual
programming capabilities
</p>
</CardContent>
</Card>
<Card className="border-purple-200 bg-purple-50/50">
<CardHeader>
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-purple-100">
<svg
className="h-6 w-6 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<CardTitle>Real-time Control</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">
Fine-grained, real-time control of scripted experimental
runs with multiple robot platforms
</p>
</CardContent>
</Card>
<Card className="border-orange-200 bg-orange-50/50">
<CardHeader>
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-orange-100">
<svg
className="h-6 w-6 text-orange-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div>
<CardTitle>Data Management</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">
Comprehensive data collection and logging with structured
storage and retrieval
</p>
</CardContent>
</Card>
<Card className="border-teal-200 bg-teal-50/50">
<CardHeader>
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-teal-100">
<svg
className="h-6 w-6 text-teal-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
/>
</svg>
</div>
<CardTitle>Platform Agnostic</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">
Support for wide range of robot hardware through RESTful
APIs, ROS, and custom plugins
</p>
</CardContent>
</Card>
<Card className="border-indigo-200 bg-indigo-50/50">
<CardHeader>
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-100">
<svg
className="h-6 w-6 text-indigo-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
<CardTitle>Collaboration Support</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-600">
Role-based access control and data sharing for effective
research team collaboration
</p>
</CardContent>
</Card>
</div>
</div>
</div>
</section>
{/* Architecture Section */}
<section className="bg-white/50 py-20">
<div className="container mx-auto px-4">
<div className="mx-auto max-w-4xl">
<div className="mb-12 text-center">
<h2 className="mb-4 text-3xl font-bold text-slate-900">
Three-Layer Architecture
</h2>
<p className="text-lg text-slate-600">
Modular web application with clear separation of concerns
</p>
</div>
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<div className="h-3 w-3 rounded-full bg-blue-500"></div>
<span>User Interface Layer</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="rounded-lg bg-blue-50 p-4 text-center">
<h4 className="font-semibold text-blue-900">
Experiment Designer
</h4>
<p className="mt-1 text-sm text-blue-700">
Visual programming for experimental protocols
</p>
</div>
<div className="rounded-lg bg-blue-50 p-4 text-center">
<h4 className="font-semibold text-blue-900">
Wizard Interface
</h4>
<p className="mt-1 text-sm text-blue-700">
Real-time control during trial execution
</p>
</div>
<div className="rounded-lg bg-blue-50 p-4 text-center">
<h4 className="font-semibold text-blue-900">
Playback & Analysis
</h4>
<p className="mt-1 text-sm text-blue-700">
Data exploration and visualization
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<div className="h-3 w-3 rounded-full bg-green-500"></div>
<span>Data Management Layer</span>
</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4 text-slate-600">
Secure database functionality with role-based access control
(Researcher, Wizard, Observer) for organizing experiment
definitions, metadata, and media assets.
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">PostgreSQL</Badge>
<Badge variant="secondary">MinIO Storage</Badge>
<Badge variant="secondary">Role-based Access</Badge>
<Badge variant="secondary">Cloud/On-premise</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<div className="h-3 w-3 rounded-full bg-purple-500"></div>
<span>Robot Integration Layer</span>
</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4 text-slate-600">
Robot-agnostic communication layer supporting multiple
integration methods for diverse hardware platforms.
</p>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">RESTful APIs</Badge>
<Badge variant="secondary">ROS Integration</Badge>
<Badge variant="secondary">Custom Plugins</Badge>
<Badge variant="secondary">Docker Deployment</Badge>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</section>
{/* Workflow Section */}
<section className="py-20">
<div className="container mx-auto px-4">
<div className="mx-auto max-w-4xl">
<div className="mb-12 text-center">
<h2 className="mb-4 text-3xl font-bold text-slate-900">
Hierarchical Experiment Structure
</h2>
<p className="text-lg text-slate-600">
Standardized terminology and organization for reproducible
research
</p>
</div>
<div className="relative">
{/* Hierarchy visualization */}
<div className="space-y-6">
<Card className="border-l-4 border-l-blue-500">
<CardContent className="pt-6">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-sm font-semibold text-blue-600">
1
</div>
<div>
<h3 className="font-semibold">Study</h3>
<p className="text-sm text-slate-600">
Top-level container comprising one or more experiments
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="ml-8 border-l-4 border-l-green-500">
<CardContent className="pt-6">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 text-sm font-semibold text-green-600">
2
</div>
<div>
<h3 className="font-semibold">Experiment</h3>
<p className="text-sm text-slate-600">
Parameterized template specifying experimental
protocol
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="ml-16 border-l-4 border-l-orange-500">
<CardContent className="pt-6">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-100 text-sm font-semibold text-orange-600">
3
</div>
<div>
<h3 className="font-semibold">Trial</h3>
<p className="text-sm text-slate-600">
Executable instance with specific participant and
conditions
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="ml-24 border-l-4 border-l-purple-500">
<CardContent className="pt-6">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-100 text-sm font-semibold text-purple-600">
4
</div>
<div>
<h3 className="font-semibold">Step</h3>
<p className="text-sm text-slate-600">
Distinct phase containing wizard or robot instructions
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="ml-32 border-l-4 border-l-pink-500">
<CardContent className="pt-6">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-pink-100 text-sm font-semibold text-pink-600">
5
</div>
<div>
<h3 className="font-semibold">Action</h3>
<p className="text-sm text-slate-600">
Specific atomic task (speech, movement, input
gathering, etc.)
</p>
</div>
</div>
</CardContent>
</Card>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</section>
{/* CTA Section */} {/* Features Bento Grid */}
<section className="bg-gradient-to-r from-blue-600 to-purple-600 py-20"> <section id="features" className="container mx-auto px-4 py-24">
<div className="container mx-auto px-4"> <div className="mb-12 text-center">
<div className="mx-auto max-w-4xl text-center text-white"> <h2 className="text-3xl font-bold tracking-tight md:text-4xl">Everything You Need</h2>
<h2 className="mb-4 text-3xl font-bold"> <p className="mt-4 text-lg text-muted-foreground">Built for the specific needs of HRI researchers and wizards.</p>
Ready to Revolutionize Your HRI Research? </div>
</h2>
<p className="mb-8 text-xl opacity-90"> <div className="grid gap-4 md:grid-cols-3 lg:grid-cols-4 lg:grid-rows-2">
Join researchers worldwide who are using our platform to conduct {/* Visual Designer - Large Item */}
more rigorous, reproducible Wizard of Oz studies. <Card className="col-span-1 md:col-span-2 lg:col-span-2 row-span-2 flex flex-col overflow-hidden bg-gradient-to-br from-blue-500/5 to-violet-500/5 dark:from-blue-900/10 dark:to-violet-900/10">
</p> <CardHeader>
<div className="flex flex-col justify-center gap-4 sm:flex-row"> <CardTitle className="flex items-center gap-2">
<Button size="lg" variant="secondary" asChild> <LayoutTemplate className="h-5 w-5 text-blue-500" />
<Link href="/auth/signup">Get Started Free</Link> Visual Experiment Designer
</Button> </CardTitle>
<Button </CardHeader>
size="lg" <CardContent className="flex-1">
variant="outline" <p className="text-muted-foreground mb-6">
className="border-white text-white hover:bg-white hover:text-blue-600" Construct complex branching narratives without writing a single line of code.
asChild Our node-based editor handles logic, timing, and robot actions automatically.
> </p>
<Link href="/auth/signin">Sign In</Link> <div className="rounded-lg border bg-background/50 p-4 h-full min-h-[200px] flex items-center justify-center shadow-inner">
</Button> <div className="flex gap-2 items-center text-sm text-muted-foreground">
<span className="rounded bg-accent p-2">Start</span>
<ArrowRight className="h-4 w-4" />
<span className="rounded bg-primary/10 p-2 border border-primary/20 text-primary font-medium">Robot: Greet</span>
<ArrowRight className="h-4 w-4" />
<span className="rounded bg-accent p-2">Wait: 5s</span>
</div>
</div>
</CardContent>
</Card>
{/* Robot Agnostic */}
<Card className="col-span-1 md:col-span-1 lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bot className="h-5 w-5 text-green-500" />
Robot Agnostic
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Switch between robots instantly. Whether it's a NAO, Pepper, or a custom ROS2 bot,
your experiment logic remains strictly separated from hardware implementation.
</p>
</CardContent>
</Card>
{/* Role Based */}
<Card className="col-span-1 md:col-span-1 lg:col-span-1 bg-muted/30">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Lock className="h-4 w-4 text-orange-500" />
Role-Based Access
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Granular permissions for Principal Investigators, Wizards, and Observers.
</p>
</CardContent>
</Card>
{/* Data Logging */}
<Card className="col-span-1 md:col-span-1 lg:col-span-1 bg-muted/30">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Database className="h-4 w-4 text-rose-500" />
Full Traceability
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Every wizard action, automated response, and sensor reading is time-stamped and logged.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Architecture Section */}
<section id="architecture" className="border-t bg-muted/30 py-24">
<div className="container mx-auto px-4">
<div className="grid gap-12 lg:grid-cols-2 lg:gap-8 items-center">
<div>
<h2 className="text-3xl font-bold tracking-tight">Enterprise-Grade Architecture</h2>
<p className="mt-4 text-lg text-muted-foreground">
Designed for reliability and scale. HRIStudio uses a modern stack to ensure your data is safe and your experiments run smoothly.
</p>
<div className="mt-8 space-y-4">
<div className="flex gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-background border shadow-sm">
<Network className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold">3-Layer Design</h3>
<p className="text-muted-foreground">Clear separation between UI, Data, and Hardware layers for maximum stability.</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-background border shadow-sm">
<Share2 className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold">Collaborative by Default</h3>
<p className="text-muted-foreground">Real-time state synchronization allows multiple researchers to monitor a single trial.</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-background border shadow-sm">
<Settings2 className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold">ROS2 Integration</h3>
<p className="text-muted-foreground">Native support for ROS2 nodes, topics, and actions right out of the box.</p>
</div>
</div>
</div>
</div>
<div className="relative mx-auto w-full max-w-[500px]">
{/* Abstract representation of architecture */}
<div className="space-y-4 relative z-10">
<Card className="border-blue-500/20 bg-blue-500/5 relative left-0 hover:left-2 transition-all cursor-default">
<CardHeader className="pb-2">
<CardTitle className="text-blue-600 dark:text-blue-400 text-sm font-mono">APP LAYER</CardTitle>
</CardHeader>
<CardContent>
<p className="font-semibold">Next.js Dashboard + Experiment Designer</p>
</CardContent>
</Card>
<Card className="border-violet-500/20 bg-violet-500/5 relative left-4 hover:left-6 transition-all cursor-default">
<CardHeader className="pb-2">
<CardTitle className="text-violet-600 dark:text-violet-400 text-sm font-mono">DATA LAYER</CardTitle>
</CardHeader>
<CardContent>
<p className="font-semibold">PostgreSQL + MinIO + TRPC API</p>
</CardContent>
</Card>
<Card className="border-green-500/20 bg-green-500/5 relative left-8 hover:left-10 transition-all cursor-default">
<CardHeader className="pb-2">
<CardTitle className="text-green-600 dark:text-green-400 text-sm font-mono">HARDWARE LAYER</CardTitle>
</CardHeader>
<CardContent>
<p className="font-semibold">ROS2 Bridge + Robot Plugins</p>
</CardContent>
</Card>
</div>
{/* Decorative blobs */}
<div className="absolute top-1/2 left-1/2 -z-10 h-[300px] w-[300px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/10 blur-3xl" />
</div>
</div> </div>
</div> </div>
</div> </section>
</section>
{/* Footer */} {/* CTA Section */}
<footer className="bg-slate-900 py-12"> <section className="container mx-auto px-4 py-24 text-center">
<div className="container mx-auto px-4"> <h2 className="text-3xl font-bold tracking-tight md:text-4xl">Ready to upgrade your lab?</h2>
<div className="text-center text-slate-400"> <p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
<div className="mb-4 flex items-center justify-center"> Join the community of researchers building the future of HRI with reproducible, open-source tools.
<Logo </p>
iconSize="md" <div className="mt-8">
showText={true} <Button size="lg" className="h-12 px-8 text-base shadow-lg shadow-primary/20" asChild>
className="text-white [&>div]:bg-white [&>div]:text-blue-600" <Link href="/auth/signup">Get Started for Free</Link>
/> </Button>
</div> </div>
<p className="mb-4"> </section>
Advancing Human-Robot Interaction research through standardized </main>
Wizard of Oz methodologies
<footer className="border-t bg-muted/40 py-12">
<div className="container mx-auto px-4 flex flex-col items-center justify-between gap-6 md:flex-row text-center md:text-left">
<div className="flex flex-col gap-2">
<Logo iconSize="sm" showText={true} />
<p className="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()} HRIStudio. All rights reserved.
</p> </p>
<div className="flex justify-center space-x-6 text-sm"> </div>
<Link href="#" className="transition-colors hover:text-white"> <div className="flex gap-6 text-sm text-muted-foreground">
Documentation <Link href="#" className="hover:text-foreground">Privacy</Link>
</Link> <Link href="#" className="hover:text-foreground">Terms</Link>
<Link href="#" className="transition-colors hover:text-white"> <Link href="#" className="hover:text-foreground">GitHub</Link>
API Reference <Link href="#" className="hover:text-foreground">Documentation</Link>
</Link>
<Link href="#" className="transition-colors hover:text-white">
Research Papers
</Link>
<Link href="#" className="transition-colors hover:text-white">
Support
</Link>
</div>
</div> </div>
</div> </div>
</footer> </footer>
</main> </div>
); );
} }

View File

@@ -1230,6 +1230,11 @@ export const systemSettingsRelations = relations(systemSettings, ({ one }) => ({
}), }),
})); }));
export const auditLogsRelations = relations(auditLogs, ({ one }) => ({ export const auditLogsRelations = relations(auditLogs, ({ one }) => ({
user: one(users, { fields: [auditLogs.userId], references: [users.id] }), 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;