Compare commits

2 Commits

11 changed files with 292 additions and 113 deletions

View File

@@ -34,7 +34,7 @@ async function inspectExperimentSteps() {
console.log(`Step [${step.orderIndex}] ID: ${step.id}`); console.log(`Step [${step.orderIndex}] ID: ${step.id}`);
console.log(`Name: ${step.name}`); console.log(`Name: ${step.name}`);
console.log(`Type: ${step.type}`); console.log(`Type: ${step.type}`);
console.log(`NextStepId: ${step.nextStepId}`);
if (step.type === 'conditional') { if (step.type === 'conditional') {
console.log("Conditions:", JSON.stringify(step.conditions, null, 2)); console.log("Conditions:", JSON.stringify(step.conditions, null, 2));

View File

@@ -31,6 +31,12 @@ const steps = [
function simulateNextStep(currentStepIndex: number) { function simulateNextStep(currentStepIndex: number) {
const currentStep = steps[currentStepIndex]; const currentStep = steps[currentStepIndex];
if (!currentStep) {
console.log("No step found at index:", currentStepIndex);
return;
}
console.log(`\n--- Simulating Next Step from: ${currentStep.name} ---`); console.log(`\n--- Simulating Next Step from: ${currentStep.name} ---`);
console.log("Current Step Data:", JSON.stringify(currentStep, null, 2)); console.log("Current Step Data:", JSON.stringify(currentStep, null, 2));

View File

@@ -48,9 +48,15 @@ async function verifyTrpcLogic() {
// 3. Inspect Step 4 (Branch A) // 3. Inspect Step 4 (Branch A)
// Step index 3 (0-based) is Branch A // Step index 3 (0-based) is Branch A
const branchAStep = transformedSteps[3]; const branchAStep = transformedSteps[3];
if (branchAStep) {
console.log("Step 4 (Branch A):", branchAStep.name); console.log("Step 4 (Branch A):", branchAStep.name);
console.log(" Type:", branchAStep.type); console.log(" Type:", branchAStep.type);
console.log(" Trigger:", JSON.stringify(branchAStep.trigger, null, 2)); console.log(" Trigger:", JSON.stringify(branchAStep.trigger, null, 2));
} else {
console.error("Step 4 (Branch A) not found in transformed steps!");
process.exit(1);
}
// Check conditions specifically // Check conditions specifically
const conditions = branchAStep.trigger?.conditions as any; const conditions = branchAStep.trigger?.conditions as any;
@@ -62,8 +68,12 @@ async function verifyTrpcLogic() {
// Inspect Step 5 (Branch B) for completeness // Inspect Step 5 (Branch B) for completeness
const branchBStep = transformedSteps[4]; const branchBStep = transformedSteps[4];
if (branchBStep) {
console.log("Step 5 (Branch B):", branchBStep.name); console.log("Step 5 (Branch B):", branchBStep.name);
console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2)); console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2));
} else {
console.warn("Step 5 (Branch B) not found in transformed steps.");
}
} }
verifyTrpcLogic() verifyTrpcLogic()

View File

@@ -82,6 +82,7 @@ export const columns: ColumnDef<AnalyticsTrial>[] = [
}, },
{ {
accessorKey: "participant.participantCode", accessorKey: "participant.participantCode",
id: "participantCode",
header: "Participant", header: "Participant",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium">{row.original.participant?.participantCode ?? "Unknown"}</div> <div className="font-medium">{row.original.participant?.participantCode ?? "Unknown"}</div>
@@ -229,15 +230,16 @@ 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..."
value={(table.getColumn("participant.participantCode")?.getFilterValue() as string) ?? ""} value={(table.getColumn("participantCode")?.getFilterValue() as string) ?? ""}
onChange={(event) => onChange={(event) =>
table.getColumn("participant.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">

View File

@@ -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>

View File

@@ -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" | "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;
@@ -46,6 +46,8 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
runTourSegment("dashboard"); runTourSegment("dashboard");
} else if (pathname.includes("/studies/new")) { } else if (pathname.includes("/studies/new")) {
runTourSegment("study_creation"); runTourSegment("study_creation");
} else if (pathname.includes("/participants/new")) {
runTourSegment("participant_creation");
} else if (pathname.includes("/designer")) { } else if (pathname.includes("/designer")) {
runTourSegment("designer"); runTourSegment("designer");
} else if (pathname.includes("/wizard")) { } else if (pathname.includes("/wizard")) {
@@ -56,7 +58,20 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
} }
}, [pathname]); }, [pathname]);
const runTourSegment = (segment: "dashboard" | "study_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";
@@ -134,6 +149,49 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
} }
} }
]; ];
} else if (segment === "participant_creation") {
steps = [
{
element: "#tour-participant-code",
popover: {
title: "Participant ID",
description: "Assign a unique code (e.g., P001) to identify this participant while maintaining anonymity.",
side: "right",
}
},
{
element: "#tour-participant-name",
popover: {
title: "Name (Optional)",
description: "You store their name for internal reference; analytics will use the ID.",
side: "right",
}
},
{
element: "#tour-participant-study-container",
popover: {
title: "Study Association",
description: "Link this participant to a specific research study to enable data collection.",
side: "right",
}
},
{
element: "#tour-participant-consent",
popover: {
title: "Informed Consent",
description: "Mandatory check to confirm you have obtained necessary ethical approvals and consent.",
side: "top",
}
},
{
element: "#tour-participant-submit",
popover: {
title: "Register",
description: "Create the participant record to begin scheduling trials.",
side: "top",
}
}
];
} else if (segment === "designer") { } else if (segment === "designer") {
steps = [ steps = [
{ {
@@ -189,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,
@@ -217,8 +319,10 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
// Trigger current page immediately // Trigger current page immediately
if (pathname === "/dashboard") runTourSegment("dashboard"); if (pathname === "/dashboard") runTourSegment("dashboard");
else if (pathname.includes("/studies/new")) runTourSegment("study_creation"); else if (pathname.includes("/studies/new")) runTourSegment("study_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");
@@ -226,8 +330,10 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
if (tour === "dashboard") runTourSegment("dashboard"); if (tour === "dashboard") runTourSegment("dashboard");
if (tour === "study_creation") runTourSegment("study_creation"); if (tour === "study_creation") runTourSegment("study_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");
} }
}; };

View File

@@ -5,9 +5,8 @@ import { Upload, X, FileText, CheckCircle, AlertCircle, Loader2 } from "lucide-r
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Progress } from "~/components/ui/progress"; import { Progress } from "~/components/ui/progress";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { toast } from "~/components/ui/use-toast"; import { toast } from "sonner";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import axios from "axios";
interface ConsentUploadFormProps { interface ConsentUploadFormProps {
studyId: string; studyId: string;
@@ -37,20 +36,16 @@ export function ConsentUploadForm({
const selectedFile = e.target.files[0]; const selectedFile = e.target.files[0];
// Validate size (10MB) // Validate size (10MB)
if (selectedFile.size > 10 * 1024 * 1024) { if (selectedFile.size > 10 * 1024 * 1024) {
toast({ toast.error("File too large", {
title: "File too large",
description: "Maximum file size is 10MB", description: "Maximum file size is 10MB",
variant: "destructive",
}); });
return; return;
} }
// Validate type // Validate type
const allowedTypes = ["application/pdf", "image/png", "image/jpeg", "image/jpg"]; const allowedTypes = ["application/pdf", "image/png", "image/jpeg", "image/jpg"];
if (!allowedTypes.includes(selectedFile.type)) { if (!allowedTypes.includes(selectedFile.type)) {
toast({ toast.error("Invalid file type", {
title: "Invalid file type",
description: "Please upload a PDF, PNG, or JPG file", description: "Please upload a PDF, PNG, or JPG file",
variant: "destructive",
}); });
return; return;
} }
@@ -74,19 +69,31 @@ export function ConsentUploadForm({
size: file.size, size: file.size,
}); });
// 2. Upload to MinIO // 2. Upload to MinIO using XMLHttpRequest for progress
await axios.put(url, file, { await new Promise<void>((resolve, reject) => {
headers: { const xhr = new XMLHttpRequest();
"Content-Type": file.type, xhr.open("PUT", url, true);
}, xhr.setRequestHeader("Content-Type", file.type);
onUploadProgress: (progressEvent) => {
if (progressEvent.total) { xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percentCompleted = Math.round( const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total (event.loaded * 100) / event.total
); );
setUploadProgress(percentCompleted); setUploadProgress(percentCompleted);
} }
}, };
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Network error during upload"));
xhr.send(file);
}); });
// 3. Record Consent in DB // 3. Record Consent in DB
@@ -96,18 +103,15 @@ export function ConsentUploadForm({
storagePath: key, storagePath: key,
}); });
toast({ toast.success("Consent Recorded", {
title: "Consent Recorded",
description: "The consent form has been uploaded and recorded successfully.", description: "The consent form has been uploaded and recorded successfully.",
}); });
onSuccess(); onSuccess();
} catch (error) { } catch (error) {
console.error("Upload failed:", error); console.error("Upload failed:", error);
toast({ toast.error("Upload Failed", {
title: "Upload Failed",
description: error instanceof Error ? error.message : "An unexpected error occurred", description: error instanceof Error ? error.message : "An unexpected error occurred",
variant: "destructive",
}); });
setIsUploading(false); setIsUploading(false);
} }

View File

@@ -26,6 +26,8 @@ import {
} from "~/components/ui/select"; } from "~/components/ui/select";
import { useStudyContext } from "~/lib/study-context"; import { useStudyContext } from "~/lib/study-context";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { useTour } from "~/components/onboarding/TourProvider";
import { Button } from "~/components/ui/button";
type DemographicsData = { type DemographicsData = {
age?: number; age?: number;
@@ -80,6 +82,7 @@ export function ParticipantForm({
studyId, studyId,
}: ParticipantFormProps) { }: ParticipantFormProps) {
const router = useRouter(); const router = useRouter();
const { startTour } = useTour();
const { selectedStudyId } = useStudyContext(); const { selectedStudyId } = useStudyContext();
const contextStudyId = studyId ?? selectedStudyId; const contextStudyId = studyId ?? selectedStudyId;
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -262,7 +265,7 @@ export function ParticipantForm({
<FormField> <FormField>
<Label htmlFor="participantCode">Participant Code *</Label> <Label htmlFor="participantCode">Participant Code *</Label>
<Input <Input
id="participantCode" id="tour-participant-code"
{...form.register("participantCode")} {...form.register("participantCode")}
placeholder="e.g., P001" placeholder="e.g., P001"
className={ className={
@@ -279,7 +282,7 @@ export function ParticipantForm({
<FormField> <FormField>
<Label htmlFor="name">Full Name</Label> <Label htmlFor="name">Full Name</Label>
<Input <Input
id="name" id="tour-participant-name"
{...form.register("name")} {...form.register("name")}
placeholder="Optional name" placeholder="Optional name"
className={form.formState.errors.name ? "border-red-500" : ""} className={form.formState.errors.name ? "border-red-500" : ""}
@@ -317,7 +320,8 @@ export function ParticipantForm({
> >
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<FormField> <FormField>
<Label htmlFor="studyId">Study *</Label> <Label htmlFor="studyId" id="tour-participant-study-label">Study *</Label>
<div id="tour-participant-study-container">
<Select <Select
value={form.watch("studyId")} value={form.watch("studyId")}
onValueChange={(value) => form.setValue("studyId", value)} onValueChange={(value) => form.setValue("studyId", value)}
@@ -347,6 +351,7 @@ export function ParticipantForm({
{form.formState.errors.studyId.message} {form.formState.errors.studyId.message}
</p> </p>
)} )}
</div>
</FormField> </FormField>
<FormField> <FormField>
@@ -408,7 +413,7 @@ export function ParticipantForm({
<FormField> <FormField>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="consentGiven" id="tour-participant-consent"
checked={form.watch("consentGiven")} checked={form.watch("consentGiven")}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
form.setValue("consentGiven", !!checked) form.setValue("consentGiven", !!checked)
@@ -495,6 +500,17 @@ export function ParticipantForm({
isDeleting={isDeleting} isDeleting={isDeleting}
sidebar={mode === "create" ? sidebar : undefined} sidebar={mode === "create" ? sidebar : undefined}
submitText={mode === "create" ? "Register Participant" : "Save Changes"} submitText={mode === "create" ? "Register Participant" : "Save Changes"}
submitButtonId="tour-participant-submit"
extraActions={
mode === "create" ? (
<Button variant="ghost" size="sm" onClick={() => startTour("participant_creation")}>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Help</span>
<div className="flex h-5 w-5 items-center justify-center rounded-full border text-xs text-muted-foreground">?</div>
</div>
</Button>
) : undefined
}
> >
{formFields} {formFields}
</EntityForm> </EntityForm>

View File

@@ -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" />

View File

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

View File

@@ -225,7 +225,7 @@ export function WizardExecutionPanel({
<div className="relative ml-3 space-y-0 pt-2"> <div className="relative ml-3 space-y-0 pt-2">
{currentStep.actions.map((action, idx) => { {currentStep.actions.map((action, idx) => {
const isCompleted = idx < activeActionIndex; const isCompleted = idx < activeActionIndex;
const isActive = idx === activeActionIndex; const isActive: boolean = idx === activeActionIndex;
const isLast = idx === currentStep.actions!.length - 1; const isLast = idx === currentStep.actions!.length - 1;
return ( return (
@@ -281,7 +281,7 @@ export function WizardExecutionPanel({
)} )}
{/* Active Action Controls */} {/* Active Action Controls */}
{isActive && ( {isActive === true ? (
<div className="pt-3 flex items-center gap-3"> <div className="pt-3 flex items-center gap-3">
{action.pluginId && !["hristudio-core", "hristudio-woz"].includes(action.pluginId) ? ( {action.pluginId && !["hristudio-core", "hristudio-woz"].includes(action.pluginId) ? (
<> <>
@@ -339,16 +339,29 @@ export function WizardExecutionPanel({
</Button> </Button>
)} )}
</div> </div>
)} ) : null}
{/* Wizard Wait For Response / Branching UI */} {/* Wizard Wait For Response / Branching UI */}
{isActive && action.type === 'wizard_wait_for_response' && action.parameters?.options && Array.isArray(action.parameters.options) && ( {isActive === true &&
action.type === "wizard_wait_for_response" &&
action.parameters?.options &&
Array.isArray(action.parameters.options) ? (
<div className="pt-3 grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="pt-3 grid grid-cols-1 sm:grid-cols-2 gap-2">
{(action.parameters.options as any[]).map((opt, optIdx) => { {(action.parameters.options as any[]).map(
(opt, optIdx) => {
// Handle both string options and object options // Handle both string options and object options
const label = typeof opt === 'string' ? opt : opt.label; const label =
const value = typeof opt === 'string' ? opt : opt.value; typeof opt === "string"
const nextStepId = typeof opt === 'object' ? opt.nextStepId : undefined; ? opt
: opt.label;
const value =
typeof opt === "string"
? opt
: opt.value;
const nextStepId =
typeof opt === "object"
? opt.nextStepId
: undefined;
return ( return (
<Button <Button
@@ -357,27 +370,31 @@ export function WizardExecutionPanel({
className="justify-start h-auto py-3 px-4 text-left border-primary/20 hover:border-primary hover:bg-primary/5" className="justify-start h-auto py-3 px-4 text-left border-primary/20 hover:border-primary hover:bg-primary/5"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
onExecuteAction( onExecuteAction(action.id, {
action.id,
{
value, value,
label, label,
nextStepId nextStepId,
} });
);
onActionCompleted(); onActionCompleted();
}} }}
disabled={readOnly || isExecuting} disabled={readOnly || isExecuting}
> >
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<span className="font-medium">{String(label)}</span> <span className="font-medium">
{typeof opt !== 'string' && value && <span className="text-xs text-muted-foreground font-mono bg-muted px-1.5 py-0.5 rounded-sm">{String(value)}</span>} {String(label)}
</span>
{typeof opt !== "string" && value && (
<span className="text-xs text-muted-foreground font-mono bg-muted px-1.5 py-0.5 rounded-sm">
{String(value)}
</span>
)}
</div> </div>
</Button> </Button>
); );
})} }
</div>
)} )}
</div>
) : null}
{/* Completed State Actions */} {/* Completed State Actions */}
{isCompleted && action.pluginId && ( {isCompleted && action.pluginId && (