mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
feat: Add guided tour functionality for analytics and wizard components, including new tour steps and triggers.
This commit is contained in:
@@ -230,7 +230,7 @@ export function StudyAnalyticsDataTable({ data }: StudyAnalyticsDataTableProps)
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full" id="tour-analytics-table">
|
||||||
<div className="flex items-center py-4">
|
<div className="flex items-center py-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter participants..."
|
placeholder="Filter participants..."
|
||||||
@@ -239,6 +239,7 @@ export function StudyAnalyticsDataTable({ data }: StudyAnalyticsDataTableProps)
|
|||||||
table.getColumn("participantCode")?.setFilterValue(event.target.value)
|
table.getColumn("participantCode")?.setFilterValue(event.target.value)
|
||||||
}
|
}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
|
id="tour-analytics-filter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
|
|||||||
@@ -1192,6 +1192,11 @@ export function DesignerRoot({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium">Flow Workspace</span>
|
<span className="text-sm font-medium">Flow Workspace</span>
|
||||||
|
{rightCollapsed && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => startTour('designer')}>
|
||||||
|
<HelpCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
{rightCollapsed && (
|
{rightCollapsed && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1204,6 +1209,8 @@ export function DesignerRoot({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex-1 overflow-hidden min-h-0 relative">
|
<div className="flex-1 overflow-hidden min-h-0 relative">
|
||||||
{centerPanel}
|
{centerPanel}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTheme } from "next-themes";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
type TourType = "dashboard" | "study_creation" | "participant_creation" | "designer" | "wizard" | "full_platform";
|
type TourType = "dashboard" | "study_creation" | "participant_creation" | "designer" | "wizard" | "analytics" | "full_platform";
|
||||||
|
|
||||||
interface TourContextType {
|
interface TourContextType {
|
||||||
startTour: (tour: TourType) => void;
|
startTour: (tour: TourType) => void;
|
||||||
@@ -58,7 +58,20 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const runTourSegment = (segment: "dashboard" | "study_creation" | "participant_creation" | "designer" | "wizard") => {
|
useEffect(() => {
|
||||||
|
// Listen for custom tour triggers (from components without context access)
|
||||||
|
const handleTourTrigger = (e: Event) => {
|
||||||
|
const detail = (e as CustomEvent).detail as TourType;
|
||||||
|
if (detail) {
|
||||||
|
startTour(detail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('hristudio-start-tour', handleTourTrigger);
|
||||||
|
return () => document.removeEventListener('hristudio-start-tour', handleTourTrigger);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const runTourSegment = (segment: "dashboard" | "study_creation" | "participant_creation" | "designer" | "wizard" | "analytics") => {
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
// We add a specific class to handle dark/light overrides reliably
|
// We add a specific class to handle dark/light overrides reliably
|
||||||
const themeClass = isDark ? "driverjs-theme-dark" : "driverjs-theme-light";
|
const themeClass = isDark ? "driverjs-theme-dark" : "driverjs-theme-light";
|
||||||
@@ -234,6 +247,50 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
else if (segment === "analytics") {
|
||||||
|
steps = [
|
||||||
|
{
|
||||||
|
element: "#tour-analytics-table",
|
||||||
|
popover: {
|
||||||
|
title: "Study Analytics",
|
||||||
|
description: "View aggregate data across all participant sessions. Sort and filter to identify trends or specific trials.",
|
||||||
|
side: "bottom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#tour-analytics-filter",
|
||||||
|
popover: {
|
||||||
|
title: "Filter Data",
|
||||||
|
description: "Quickly find participants by ID or name using this search bar.",
|
||||||
|
side: "bottom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#tour-trial-metrics",
|
||||||
|
popover: {
|
||||||
|
title: "Trial Metrics",
|
||||||
|
description: "High-level KPIs for the selected trial: Duration, Robot Actions, and Intervention counts.",
|
||||||
|
side: "bottom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#tour-trial-timeline",
|
||||||
|
popover: {
|
||||||
|
title: "Video & Timeline",
|
||||||
|
description: "Watch the trial recording synced with the event timeline. Click any event to jump to that moment in the video.",
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#tour-trial-events",
|
||||||
|
popover: {
|
||||||
|
title: "Event Log",
|
||||||
|
description: "A detailed, searchable log of every system event, robot action, and wizard interaction.",
|
||||||
|
side: "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
driverObj.current = driver({
|
driverObj.current = driver({
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
@@ -265,6 +322,7 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
|||||||
else if (pathname.includes("/participants/new")) runTourSegment("participant_creation");
|
else if (pathname.includes("/participants/new")) runTourSegment("participant_creation");
|
||||||
else if (pathname.includes("/designer")) runTourSegment("designer");
|
else if (pathname.includes("/designer")) runTourSegment("designer");
|
||||||
else if (pathname.includes("/wizard")) runTourSegment("wizard");
|
else if (pathname.includes("/wizard")) runTourSegment("wizard");
|
||||||
|
else if (pathname.includes("/analysis")) runTourSegment("analytics");
|
||||||
else runTourSegment("dashboard"); // Fallback
|
else runTourSegment("dashboard"); // Fallback
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem("hristudio_tour_mode", "manual");
|
localStorage.setItem("hristudio_tour_mode", "manual");
|
||||||
@@ -275,6 +333,7 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (tour === "participant_creation") runTourSegment("participant_creation");
|
if (tour === "participant_creation") runTourSegment("participant_creation");
|
||||||
if (tour === "designer") runTourSegment("designer");
|
if (tour === "designer") runTourSegment("designer");
|
||||||
if (tour === "wizard") runTourSegment("wizard");
|
if (tour === "wizard") runTourSegment("wizard");
|
||||||
|
if (tour === "analytics") runTourSegment("analytics");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,17 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
|||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 ml-1" onClick={() => {
|
||||||
|
// Dispatch custom event since useTour isn't directly available in this specific context yet
|
||||||
|
// or better yet, assume we can import useTour if valid context, but here let's try direct button if applicable.
|
||||||
|
// Actually, TrialAnalysisView is a child of page, we need useTour context.
|
||||||
|
// Checking imports... TrialAnalysisView doesn't have useTour.
|
||||||
|
// We should probably just dispatch an event or rely on the parent.
|
||||||
|
// Let's assume we can add useTour hook support here.
|
||||||
|
document.dispatchEvent(new CustomEvent('hristudio-start-tour', { detail: 'analytics' }));
|
||||||
|
}}>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h1 className="text-lg font-semibold leading-none tracking-tight">
|
<h1 className="text-lg font-semibold leading-none tracking-tight">
|
||||||
{trial.experiment.name}
|
{trial.experiment.name}
|
||||||
@@ -82,7 +93,7 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics Header */}
|
{/* Metrics Header */}
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4" id="tour-trial-metrics">
|
||||||
<Card className="bg-gradient-to-br from-blue-50 to-transparent dark:from-blue-950/20">
|
<Card className="bg-gradient-to-br from-blue-50 to-transparent dark:from-blue-950/20">
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">Duration</CardTitle>
|
<CardTitle className="text-sm font-medium text-muted-foreground">Duration</CardTitle>
|
||||||
@@ -147,7 +158,7 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
|||||||
<ResizablePanelGroup direction="vertical">
|
<ResizablePanelGroup direction="vertical">
|
||||||
|
|
||||||
{/* TOP: Video & Timeline */}
|
{/* TOP: Video & Timeline */}
|
||||||
<ResizablePanel defaultSize={50} minSize={30} className="flex flex-col min-h-0 bg-black/5 dark:bg-black/40">
|
<ResizablePanel defaultSize={50} minSize={30} className="flex flex-col min-h-0 bg-black/5 dark:bg-black/40" id="tour-trial-timeline">
|
||||||
<div className="relative flex-1 min-h-0 flex items-center justify-center">
|
<div className="relative flex-1 min-h-0 flex items-center justify-center">
|
||||||
{videoUrl ? (
|
{videoUrl ? (
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
@@ -175,7 +186,7 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
|||||||
<ResizableHandle withHandle className="bg-border/50" />
|
<ResizableHandle withHandle className="bg-border/50" />
|
||||||
|
|
||||||
{/* BOTTOM: Events Table */}
|
{/* BOTTOM: Events Table */}
|
||||||
<ResizablePanel defaultSize={50} minSize={20} className="flex flex-col min-h-0 bg-background">
|
<ResizablePanel defaultSize={50} minSize={20} className="flex flex-col min-h-0 bg-background" id="tour-trial-events">
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b">
|
<div className="flex items-center justify-between px-4 py-3 border-b">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4 text-primary" />
|
<FileText className="h-4 w-4 text-primary" />
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const WizardControlPanel = React.memo(function WizardControlPanel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col" id="tour-wizard-controls">
|
||||||
|
|
||||||
|
|
||||||
<div className="min-h-0 flex-1">
|
<div className="min-h-0 flex-1">
|
||||||
@@ -155,7 +155,7 @@ export const WizardControlPanel = React.memo(function WizardControlPanel({
|
|||||||
{/* Decision Point UI removed as per user request (handled in Execution Panel) */}
|
{/* Decision Point UI removed as per user request (handled in Execution Panel) */}
|
||||||
|
|
||||||
{trial.status === "in_progress" ? (
|
{trial.status === "in_progress" ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2" id="tour-wizard-action-list">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -225,7 +225,7 @@ export const WizardControlPanel = React.memo(function WizardControlPanel({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="autonomous-life" className="text-xs font-normal text-muted-foreground">Autonomous Life</Label>
|
<Label htmlFor="autonomous-life" className="text-xs font-normal text-muted-foreground">Autonomous Life</Label>
|
||||||
<Switch
|
<Switch
|
||||||
id="autonomous-life"
|
id="tour-wizard-autonomous"
|
||||||
checked={!!autonomousLife}
|
checked={!!autonomousLife}
|
||||||
onCheckedChange={handleAutonomousLifeChange}
|
onCheckedChange={handleAutonomousLifeChange}
|
||||||
disabled={!_isConnected || readOnly}
|
disabled={!_isConnected || readOnly}
|
||||||
|
|||||||
Reference in New Issue
Block a user