mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
Compare commits
2 Commits
85b951f742
...
568d408587
| Author | SHA1 | Date | |
|---|---|---|---|
| 568d408587 | |||
| 93de577939 |
@@ -34,7 +34,7 @@ async function inspectExperimentSteps() {
|
||||
console.log(`Step [${step.orderIndex}] ID: ${step.id}`);
|
||||
console.log(`Name: ${step.name}`);
|
||||
console.log(`Type: ${step.type}`);
|
||||
console.log(`NextStepId: ${step.nextStepId}`);
|
||||
|
||||
|
||||
if (step.type === 'conditional') {
|
||||
console.log("Conditions:", JSON.stringify(step.conditions, null, 2));
|
||||
|
||||
@@ -31,6 +31,12 @@ const steps = [
|
||||
|
||||
function simulateNextStep(currentStepIndex: number) {
|
||||
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("Current Step Data:", JSON.stringify(currentStep, null, 2));
|
||||
|
||||
|
||||
@@ -48,9 +48,15 @@ async function verifyTrpcLogic() {
|
||||
// 3. Inspect Step 4 (Branch A)
|
||||
// Step index 3 (0-based) is Branch A
|
||||
const branchAStep = transformedSteps[3];
|
||||
console.log("Step 4 (Branch A):", branchAStep.name);
|
||||
console.log(" Type:", branchAStep.type);
|
||||
console.log(" Trigger:", JSON.stringify(branchAStep.trigger, null, 2));
|
||||
|
||||
if (branchAStep) {
|
||||
console.log("Step 4 (Branch A):", branchAStep.name);
|
||||
console.log(" Type:", branchAStep.type);
|
||||
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
|
||||
const conditions = branchAStep.trigger?.conditions as any;
|
||||
@@ -62,8 +68,12 @@ async function verifyTrpcLogic() {
|
||||
|
||||
// Inspect Step 5 (Branch B) for completeness
|
||||
const branchBStep = transformedSteps[4];
|
||||
console.log("Step 5 (Branch B):", branchBStep.name);
|
||||
console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2));
|
||||
if (branchBStep) {
|
||||
console.log("Step 5 (Branch B):", branchBStep.name);
|
||||
console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2));
|
||||
} else {
|
||||
console.warn("Step 5 (Branch B) not found in transformed steps.");
|
||||
}
|
||||
}
|
||||
|
||||
verifyTrpcLogic()
|
||||
|
||||
@@ -82,6 +82,7 @@ export const columns: ColumnDef<AnalyticsTrial>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "participant.participantCode",
|
||||
id: "participantCode",
|
||||
header: "Participant",
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium">{row.original.participant?.participantCode ?? "Unknown"}</div>
|
||||
@@ -229,15 +230,16 @@ export function StudyAnalyticsDataTable({ data }: StudyAnalyticsDataTableProps)
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full" id="tour-analytics-table">
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
placeholder="Filter participants..."
|
||||
value={(table.getColumn("participant.participantCode")?.getFilterValue() as string) ?? ""}
|
||||
value={(table.getColumn("participantCode")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("participant.participantCode")?.setFilterValue(event.target.value)
|
||||
table.getColumn("participantCode")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
id="tour-analytics-filter"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md border bg-card">
|
||||
|
||||
@@ -1193,15 +1193,22 @@ export function DesignerRoot({
|
||||
)}
|
||||
<span className="text-sm font-medium">Flow Workspace</span>
|
||||
{rightCollapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 ml-2"
|
||||
onClick={() => setRightCollapsed(false)}
|
||||
title="Open Inspector"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
<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 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 ml-2"
|
||||
onClick={() => setRightCollapsed(false)}
|
||||
title="Open Inspector"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden min-h-0 relative">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTheme } from "next-themes";
|
||||
import { usePathname } from "next/navigation";
|
||||
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 {
|
||||
startTour: (tour: TourType) => void;
|
||||
@@ -46,6 +46,8 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
||||
runTourSegment("dashboard");
|
||||
} 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("/wizard")) {
|
||||
@@ -56,7 +58,20 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [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";
|
||||
// We add a specific class to handle dark/light overrides reliably
|
||||
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") {
|
||||
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({
|
||||
showProgress: true,
|
||||
@@ -217,8 +319,10 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
||||
// Trigger current page immediately
|
||||
if (pathname === "/dashboard") runTourSegment("dashboard");
|
||||
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("/wizard")) runTourSegment("wizard");
|
||||
else if (pathname.includes("/analysis")) runTourSegment("analytics");
|
||||
else runTourSegment("dashboard"); // Fallback
|
||||
} else {
|
||||
localStorage.setItem("hristudio_tour_mode", "manual");
|
||||
@@ -226,8 +330,10 @@ export function TourProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (tour === "dashboard") runTourSegment("dashboard");
|
||||
if (tour === "study_creation") runTourSegment("study_creation");
|
||||
if (tour === "participant_creation") runTourSegment("participant_creation");
|
||||
if (tour === "designer") runTourSegment("designer");
|
||||
if (tour === "wizard") runTourSegment("wizard");
|
||||
if (tour === "analytics") runTourSegment("analytics");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ import { Upload, X, FileText, CheckCircle, AlertCircle, Loader2 } from "lucide-r
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import { api } from "~/trpc/react";
|
||||
import { toast } from "~/components/ui/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "~/lib/utils";
|
||||
import axios from "axios";
|
||||
|
||||
interface ConsentUploadFormProps {
|
||||
studyId: string;
|
||||
@@ -37,20 +36,16 @@ export function ConsentUploadForm({
|
||||
const selectedFile = e.target.files[0];
|
||||
// Validate size (10MB)
|
||||
if (selectedFile.size > 10 * 1024 * 1024) {
|
||||
toast({
|
||||
title: "File too large",
|
||||
toast.error("File too large", {
|
||||
description: "Maximum file size is 10MB",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Validate type
|
||||
const allowedTypes = ["application/pdf", "image/png", "image/jpeg", "image/jpg"];
|
||||
if (!allowedTypes.includes(selectedFile.type)) {
|
||||
toast({
|
||||
title: "Invalid file type",
|
||||
toast.error("Invalid file type", {
|
||||
description: "Please upload a PDF, PNG, or JPG file",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -74,19 +69,31 @@ export function ConsentUploadForm({
|
||||
size: file.size,
|
||||
});
|
||||
|
||||
// 2. Upload to MinIO
|
||||
await axios.put(url, file, {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
// 2. Upload to MinIO using XMLHttpRequest for progress
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("PUT", url, true);
|
||||
xhr.setRequestHeader("Content-Type", file.type);
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
(event.loaded * 100) / event.total
|
||||
);
|
||||
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
|
||||
@@ -96,18 +103,15 @@ export function ConsentUploadForm({
|
||||
storagePath: key,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Consent Recorded",
|
||||
toast.success("Consent Recorded", {
|
||||
description: "The consent form has been uploaded and recorded successfully.",
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("Upload failed:", error);
|
||||
toast({
|
||||
title: "Upload Failed",
|
||||
toast.error("Upload Failed", {
|
||||
description: error instanceof Error ? error.message : "An unexpected error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsUploading(false);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
} from "~/components/ui/select";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useTour } from "~/components/onboarding/TourProvider";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
type DemographicsData = {
|
||||
age?: number;
|
||||
@@ -80,6 +82,7 @@ export function ParticipantForm({
|
||||
studyId,
|
||||
}: ParticipantFormProps) {
|
||||
const router = useRouter();
|
||||
const { startTour } = useTour();
|
||||
const { selectedStudyId } = useStudyContext();
|
||||
const contextStudyId = studyId ?? selectedStudyId;
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@@ -262,7 +265,7 @@ export function ParticipantForm({
|
||||
<FormField>
|
||||
<Label htmlFor="participantCode">Participant Code *</Label>
|
||||
<Input
|
||||
id="participantCode"
|
||||
id="tour-participant-code"
|
||||
{...form.register("participantCode")}
|
||||
placeholder="e.g., P001"
|
||||
className={
|
||||
@@ -279,7 +282,7 @@ export function ParticipantForm({
|
||||
<FormField>
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
id="tour-participant-name"
|
||||
{...form.register("name")}
|
||||
placeholder="Optional name"
|
||||
className={form.formState.errors.name ? "border-red-500" : ""}
|
||||
@@ -317,36 +320,38 @@ export function ParticipantForm({
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<FormField>
|
||||
<Label htmlFor="studyId">Study *</Label>
|
||||
<Select
|
||||
value={form.watch("studyId")}
|
||||
onValueChange={(value) => form.setValue("studyId", value)}
|
||||
disabled={studiesLoading || mode === "edit"}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={
|
||||
form.formState.errors.studyId ? "border-red-500" : ""
|
||||
}
|
||||
<Label htmlFor="studyId" id="tour-participant-study-label">Study *</Label>
|
||||
<div id="tour-participant-study-container">
|
||||
<Select
|
||||
value={form.watch("studyId")}
|
||||
onValueChange={(value) => form.setValue("studyId", value)}
|
||||
disabled={studiesLoading || mode === "edit"}
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
studiesLoading ? "Loading..." : "Select study"
|
||||
<SelectTrigger
|
||||
className={
|
||||
form.formState.errors.studyId ? "border-red-500" : ""
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{studiesData?.studies?.map((study) => (
|
||||
<SelectItem key={study.id} value={study.id}>
|
||||
{study.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{form.formState.errors.studyId && (
|
||||
<p className="text-sm text-red-600">
|
||||
{form.formState.errors.studyId.message}
|
||||
</p>
|
||||
)}
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
studiesLoading ? "Loading..." : "Select study"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{studiesData?.studies?.map((study) => (
|
||||
<SelectItem key={study.id} value={study.id}>
|
||||
{study.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{form.formState.errors.studyId && (
|
||||
<p className="text-sm text-red-600">
|
||||
{form.formState.errors.studyId.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
@@ -408,7 +413,7 @@ export function ParticipantForm({
|
||||
<FormField>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="consentGiven"
|
||||
id="tour-participant-consent"
|
||||
checked={form.watch("consentGiven")}
|
||||
onCheckedChange={(checked) =>
|
||||
form.setValue("consentGiven", !!checked)
|
||||
@@ -495,6 +500,17 @@ export function ParticipantForm({
|
||||
isDeleting={isDeleting}
|
||||
sidebar={mode === "create" ? sidebar : undefined}
|
||||
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}
|
||||
</EntityForm>
|
||||
|
||||
@@ -60,6 +60,17 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Link>
|
||||
</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">
|
||||
<h1 className="text-lg font-semibold leading-none tracking-tight">
|
||||
{trial.experiment.name}
|
||||
@@ -82,7 +93,7 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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>
|
||||
@@ -147,7 +158,7 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
||||
<ResizablePanelGroup direction="vertical">
|
||||
|
||||
{/* 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">
|
||||
{videoUrl ? (
|
||||
<div className="absolute inset-0">
|
||||
@@ -175,7 +186,7 @@ export function TrialAnalysisView({ trial, backHref }: TrialAnalysisViewProps) {
|
||||
<ResizableHandle withHandle className="bg-border/50" />
|
||||
|
||||
{/* 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 gap-2">
|
||||
<FileText className="h-4 w-4 text-primary" />
|
||||
|
||||
@@ -145,7 +145,7 @@ export const WizardControlPanel = React.memo(function WizardControlPanel({
|
||||
};
|
||||
|
||||
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">
|
||||
@@ -155,7 +155,7 @@ export const WizardControlPanel = React.memo(function WizardControlPanel({
|
||||
{/* Decision Point UI removed as per user request (handled in Execution Panel) */}
|
||||
|
||||
{trial.status === "in_progress" ? (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2" id="tour-wizard-action-list">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -225,7 +225,7 @@ export const WizardControlPanel = React.memo(function WizardControlPanel({
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="autonomous-life" className="text-xs font-normal text-muted-foreground">Autonomous Life</Label>
|
||||
<Switch
|
||||
id="autonomous-life"
|
||||
id="tour-wizard-autonomous"
|
||||
checked={!!autonomousLife}
|
||||
onCheckedChange={handleAutonomousLifeChange}
|
||||
disabled={!_isConnected || readOnly}
|
||||
|
||||
@@ -225,7 +225,7 @@ export function WizardExecutionPanel({
|
||||
<div className="relative ml-3 space-y-0 pt-2">
|
||||
{currentStep.actions.map((action, idx) => {
|
||||
const isCompleted = idx < activeActionIndex;
|
||||
const isActive = idx === activeActionIndex;
|
||||
const isActive: boolean = idx === activeActionIndex;
|
||||
const isLast = idx === currentStep.actions!.length - 1;
|
||||
|
||||
return (
|
||||
@@ -281,7 +281,7 @@ export function WizardExecutionPanel({
|
||||
)}
|
||||
|
||||
{/* Active Action Controls */}
|
||||
{isActive && (
|
||||
{isActive === true ? (
|
||||
<div className="pt-3 flex items-center gap-3">
|
||||
{action.pluginId && !["hristudio-core", "hristudio-woz"].includes(action.pluginId) ? (
|
||||
<>
|
||||
@@ -339,45 +339,62 @@ export function WizardExecutionPanel({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* 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">
|
||||
{(action.parameters.options as any[]).map((opt, optIdx) => {
|
||||
// Handle both string options and object options
|
||||
const label = typeof opt === 'string' ? opt : opt.label;
|
||||
const value = typeof opt === 'string' ? opt : opt.value;
|
||||
const nextStepId = typeof opt === 'object' ? opt.nextStepId : undefined;
|
||||
{(action.parameters.options as any[]).map(
|
||||
(opt, optIdx) => {
|
||||
// Handle both string options and object options
|
||||
const label =
|
||||
typeof opt === "string"
|
||||
? opt
|
||||
: opt.label;
|
||||
const value =
|
||||
typeof opt === "string"
|
||||
? opt
|
||||
: opt.value;
|
||||
const nextStepId =
|
||||
typeof opt === "object"
|
||||
? opt.nextStepId
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={optIdx}
|
||||
variant="outline"
|
||||
className="justify-start h-auto py-3 px-4 text-left border-primary/20 hover:border-primary hover:bg-primary/5"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onExecuteAction(
|
||||
action.id,
|
||||
{
|
||||
return (
|
||||
<Button
|
||||
key={optIdx}
|
||||
variant="outline"
|
||||
className="justify-start h-auto py-3 px-4 text-left border-primary/20 hover:border-primary hover:bg-primary/5"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onExecuteAction(action.id, {
|
||||
value,
|
||||
label,
|
||||
nextStepId
|
||||
}
|
||||
);
|
||||
onActionCompleted();
|
||||
}}
|
||||
disabled={readOnly || isExecuting}
|
||||
>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<span className="font-medium">{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>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
nextStepId,
|
||||
});
|
||||
onActionCompleted();
|
||||
}}
|
||||
disabled={readOnly || isExecuting}
|
||||
>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<span className="font-medium">
|
||||
{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>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Completed State Actions */}
|
||||
{isCompleted && action.pluginId && (
|
||||
|
||||
Reference in New Issue
Block a user