From bbbe397ba8fce895d1dea6ebf777904a6e777635 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Sat, 21 Mar 2026 20:21:18 -0400 Subject: [PATCH] Various improvements: study forms, participant management, PDF generator, robot integration --- scripts/archive/seed-story-red-rock.ts | 239 +++++-- scripts/migrate-add-identifier.ts | 12 +- .../(dashboard)/studies/[id]/forms/page.tsx | 628 ++++++++++-------- src/app/(dashboard)/studies/[id]/page.tsx | 13 +- src/app/api/upload/route.ts | 13 +- src/components/dashboard/app-sidebar.tsx | 6 +- .../participants/DigitalSignatureModal.tsx | 421 ++++++------ .../ParticipantConsentManager.tsx | 25 +- .../participants/ParticipantForm.tsx | 86 +-- .../trials/wizard/WizardInterface.tsx | 17 +- src/hooks/useWizardRos.ts | 3 +- src/lib/pdf-generator.ts | 99 +-- src/server/api/routers/robots.ts | 45 +- src/server/services/robot-communication.ts | 2 +- src/server/services/trial-execution.ts | 2 +- 15 files changed, 936 insertions(+), 675 deletions(-) diff --git a/scripts/archive/seed-story-red-rock.ts b/scripts/archive/seed-story-red-rock.ts index 6f2e5ab..d0c83a1 100644 --- a/scripts/archive/seed-story-red-rock.ts +++ b/scripts/archive/seed-story-red-rock.ts @@ -35,7 +35,8 @@ async function main() { .values({ studyId: study.id, name: "Story: Red Rock", - description: "A story about a red rock on Mars with comprehension check and branching.", + description: + "A story about a red rock on Mars with comprehension check and branching.", version: 1, status: "draft", robotId: robot.id, @@ -53,102 +54,212 @@ async function main() { const checkId = uuidv4(); // Step 1: The Hook - const [step1] = await db.insert(schema.steps).values({ - experimentId: experiment.id, - name: "The Hook", - type: "wizard", - orderIndex: 0, - }).returning(); + const [step1] = await db + .insert(schema.steps) + .values({ + experimentId: experiment.id, + name: "The Hook", + type: "wizard", + orderIndex: 0, + }) + .returning(); // Step 2: The Narrative - const [step2] = await db.insert(schema.steps).values({ - experimentId: experiment.id, - name: "The Narrative", - type: "wizard", - orderIndex: 1, - }).returning(); + const [step2] = await db + .insert(schema.steps) + .values({ + experimentId: experiment.id, + name: "The Narrative", + type: "wizard", + orderIndex: 1, + }) + .returning(); // Step 3: Comprehension Check (Conditional) - const [step3] = await db.insert(schema.steps).values({ - id: checkId, - experimentId: experiment.id, - name: "Comprehension Check", - type: "conditional", - orderIndex: 2, - conditions: { - variable: "last_wizard_response", - options: [ - { label: "Answer: Red (Correct)", value: "Red", variant: "default", nextStepId: branchAId }, - { label: "Answer: Other (Incorrect)", value: "Incorrect", variant: "destructive", nextStepId: branchBId } - ] - } - }).returning(); + const [step3] = await db + .insert(schema.steps) + .values({ + id: checkId, + experimentId: experiment.id, + name: "Comprehension Check", + type: "conditional", + orderIndex: 2, + conditions: { + variable: "last_wizard_response", + options: [ + { + label: "Answer: Red (Correct)", + value: "Red", + variant: "default", + nextStepId: branchAId, + }, + { + label: "Answer: Other (Incorrect)", + value: "Incorrect", + variant: "destructive", + nextStepId: branchBId, + }, + ], + }, + }) + .returning(); // Step 4: Branch A (Correct) - const [step4] = await db.insert(schema.steps).values({ - id: branchAId, - experimentId: experiment.id, - name: "Branch A: Correct Response", - type: "wizard", - orderIndex: 3, - conditions: { nextStepId: conclusionId } // SKIP BRANCH B - }).returning(); + const [step4] = await db + .insert(schema.steps) + .values({ + id: branchAId, + experimentId: experiment.id, + name: "Branch A: Correct Response", + type: "wizard", + orderIndex: 3, + conditions: { nextStepId: conclusionId }, // SKIP BRANCH B + }) + .returning(); // Step 5: Branch B (Incorrect) - const [step5] = await db.insert(schema.steps).values({ - id: branchBId, - experimentId: experiment.id, - name: "Branch B: Incorrect Response", - type: "wizard", - orderIndex: 4, - conditions: { nextStepId: conclusionId } - }).returning(); + const [step5] = await db + .insert(schema.steps) + .values({ + id: branchBId, + experimentId: experiment.id, + name: "Branch B: Incorrect Response", + type: "wizard", + orderIndex: 4, + conditions: { nextStepId: conclusionId }, + }) + .returning(); // Step 6: Conclusion - const [step6] = await db.insert(schema.steps).values({ - id: conclusionId, - experimentId: experiment.id, - name: "Conclusion", - type: "wizard", - orderIndex: 5, - }).returning(); + const [step6] = await db + .insert(schema.steps) + .values({ + id: conclusionId, + experimentId: experiment.id, + name: "Conclusion", + type: "wizard", + orderIndex: 5, + }) + .returning(); // 4. Create Actions // The Hook await db.insert(schema.actions).values([ - { stepId: step1!.id, name: "Say Hello", type: "nao6-ros2.say_text", orderIndex: 0, parameters: { text: "Hello! Are you ready for a story?" } }, - { stepId: step1!.id, name: "Wave", type: "nao6-ros2.move_arm", orderIndex: 1, parameters: { arm: "right", shoulder_pitch: 0.5 } } + { + stepId: step1!.id, + name: "Say Hello", + type: "nao6-ros2.say_text", + orderIndex: 0, + parameters: { text: "Hello! Are you ready for a story?" }, + }, + { + stepId: step1!.id, + name: "Wave", + type: "nao6-ros2.move_arm", + orderIndex: 1, + parameters: { arm: "right", shoulder_pitch: 0.5 }, + }, ]); // The Narrative await db.insert(schema.actions).values([ - { stepId: step2!.id, name: "The Story", type: "nao6-ros2.say_text", orderIndex: 0, parameters: { text: "Once, a traveler went to Mars. He found a bright red rock that glowed." } }, - { stepId: step2!.id, name: "Look Left", type: "nao6-ros2.turn_head", orderIndex: 1, parameters: { yaw: 0.5, speed: 0.3 } }, - { stepId: step2!.id, name: "Look Right", type: "nao6-ros2.turn_head", orderIndex: 2, parameters: { yaw: -0.5, speed: 0.3 } } + { + stepId: step2!.id, + name: "The Story", + type: "nao6-ros2.say_text", + orderIndex: 0, + parameters: { + text: "Once, a traveler went to Mars. He found a bright red rock that glowed.", + }, + }, + { + stepId: step2!.id, + name: "Look Left", + type: "nao6-ros2.turn_head", + orderIndex: 1, + parameters: { yaw: 0.5, speed: 0.3 }, + }, + { + stepId: step2!.id, + name: "Look Right", + type: "nao6-ros2.turn_head", + orderIndex: 2, + parameters: { yaw: -0.5, speed: 0.3 }, + }, ]); // Comprehension Check await db.insert(schema.actions).values([ - { stepId: step3!.id, name: "Ask Color", type: "nao6-ros2.say_text", orderIndex: 0, parameters: { text: "What color was the rock I found on Mars?" } }, - { stepId: step3!.id, name: "Wait for Color", type: "wizard_wait_for_response", orderIndex: 1, parameters: { options: ["Red", "Blue", "Green", "Incorrect"], prompt_text: "What color did the participant say?" } } + { + stepId: step3!.id, + name: "Ask Color", + type: "nao6-ros2.say_text", + orderIndex: 0, + parameters: { text: "What color was the rock I found on Mars?" }, + }, + { + stepId: step3!.id, + name: "Wait for Color", + type: "wizard_wait_for_response", + orderIndex: 1, + parameters: { + options: ["Red", "Blue", "Green", "Incorrect"], + prompt_text: "What color did the participant say?", + }, + }, ]); // Branch A (Using say_with_emotion) - await db.insert(schema.actions).values([ - { stepId: step4!.id, name: "Happy Response", type: "nao6-ros2.say_with_emotion", orderIndex: 0, parameters: { text: "Exacty! It was a glowing red rock.", emotion: "happy" } } - ]); + await db + .insert(schema.actions) + .values([ + { + stepId: step4!.id, + name: "Happy Response", + type: "nao6-ros2.say_with_emotion", + orderIndex: 0, + parameters: { + text: "Exacty! It was a glowing red rock.", + emotion: "happy", + }, + }, + ]); // Branch B await db.insert(schema.actions).values([ - { stepId: step5!.id, name: "Correct them", type: "nao6-ros2.say_text", orderIndex: 0, parameters: { text: "Actually, it was red." } }, - { stepId: step5!.id, name: "Shake Head", type: "nao6-ros2.turn_head", orderIndex: 1, parameters: { yaw: 0.3, speed: 0.5 } } + { + stepId: step5!.id, + name: "Correct them", + type: "nao6-ros2.say_text", + orderIndex: 0, + parameters: { text: "Actually, it was red." }, + }, + { + stepId: step5!.id, + name: "Shake Head", + type: "nao6-ros2.turn_head", + orderIndex: 1, + parameters: { yaw: 0.3, speed: 0.5 }, + }, ]); // Conclusion await db.insert(schema.actions).values([ - { stepId: step6!.id, name: "Final Goodbye", type: "nao6-ros2.say_text", orderIndex: 0, parameters: { text: "That is all for today. Goodbye!" } }, - { stepId: step6!.id, name: "Rest", type: "nao6-ros2.move_arm", orderIndex: 1, parameters: { shoulder_pitch: 1.5 } } + { + stepId: step6!.id, + name: "Final Goodbye", + type: "nao6-ros2.say_text", + orderIndex: 0, + parameters: { text: "That is all for today. Goodbye!" }, + }, + { + stepId: step6!.id, + name: "Rest", + type: "nao6-ros2.move_arm", + orderIndex: 1, + parameters: { shoulder_pitch: 1.5 }, + }, ]); console.log("✅ Seed completed successfully!"); diff --git a/scripts/migrate-add-identifier.ts b/scripts/migrate-add-identifier.ts index 1d6b34d..0c6246a 100644 --- a/scripts/migrate-add-identifier.ts +++ b/scripts/migrate-add-identifier.ts @@ -5,21 +5,27 @@ async function migrate() { console.log("Adding identifier column to hs_plugin..."); try { - await db.execute(sql`ALTER TABLE hs_plugin ADD COLUMN identifier varchar(100)`); + await db.execute( + sql`ALTER TABLE hs_plugin ADD COLUMN identifier varchar(100)`, + ); console.log("✓ Added identifier column"); } catch (e: any) { console.log("Column may already exist:", e.message); } try { - await db.execute(sql`UPDATE hs_plugin SET identifier = name WHERE identifier IS NULL`); + await db.execute( + sql`UPDATE hs_plugin SET identifier = name WHERE identifier IS NULL`, + ); console.log("✓ Copied name to identifier"); } catch (e: any) { console.log("Error copying:", e.message); } try { - await db.execute(sql`ALTER TABLE hs_plugin ADD CONSTRAINT hs_plugin_identifier_unique UNIQUE (identifier)`); + await db.execute( + sql`ALTER TABLE hs_plugin ADD CONSTRAINT hs_plugin_identifier_unique UNIQUE (identifier)`, + ); console.log("✓ Added unique constraint"); } catch (e: any) { console.log("Constraint may already exist:", e.message); diff --git a/src/app/(dashboard)/studies/[id]/forms/page.tsx b/src/app/(dashboard)/studies/[id]/forms/page.tsx index 79e437b..f04e6e3 100644 --- a/src/app/(dashboard)/studies/[id]/forms/page.tsx +++ b/src/app/(dashboard)/studies/[id]/forms/page.tsx @@ -3,12 +3,20 @@ import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; import { notFound } from "next/navigation"; -import { FileText, Loader2, Plus, Download, Edit2, Eye, Save } from "lucide-react"; import { - EntityView, - EntityViewHeader, - EntityViewSection, - EmptyState, + FileText, + Loader2, + Plus, + Download, + Edit2, + Eye, + Save, +} from "lucide-react"; +import { + EntityView, + EntityViewHeader, + EntityViewSection, + EmptyState, } from "~/components/ui/entity-view"; import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; import { Button } from "~/components/ui/button"; @@ -16,302 +24,346 @@ import { Badge } from "~/components/ui/badge"; import { api } from "~/trpc/react"; import { toast } from "sonner"; import { PageHeader } from "~/components/ui/page-header"; -import { useEditor, EditorContent } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import { Markdown } from 'tiptap-markdown'; -import { Table } from '@tiptap/extension-table'; -import { TableRow } from '@tiptap/extension-table-row'; -import { TableCell } from '@tiptap/extension-table-cell'; -import { TableHeader } from '@tiptap/extension-table-header'; -import { Bold, Italic, List, ListOrdered, Heading1, Heading2, Quote, Table as TableIcon } from "lucide-react"; +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { Markdown } from "tiptap-markdown"; +import { Table } from "@tiptap/extension-table"; +import { TableRow } from "@tiptap/extension-table-row"; +import { TableCell } from "@tiptap/extension-table-cell"; +import { TableHeader } from "@tiptap/extension-table-header"; +import { + Bold, + Italic, + List, + ListOrdered, + Heading1, + Heading2, + Quote, + Table as TableIcon, +} from "lucide-react"; import { downloadPdfFromHtml } from "~/lib/pdf-generator"; const Toolbar = ({ editor }: { editor: any }) => { - if (!editor) { - return null; - } + if (!editor) { + return null; + } - return ( -
- - -
- - -
- - - -
- -
- ); + return ( +
+ + +
+ + +
+ + + +
+ +
+ ); }; interface StudyFormsPageProps { - params: Promise<{ - id: string; - }>; + params: Promise<{ + id: string; + }>; } export default function StudyFormsPage({ params }: StudyFormsPageProps) { - const { data: session } = useSession(); - const utils = api.useUtils(); - const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(null); - const [editorTarget, setEditorTarget] = useState(""); + const { data: session } = useSession(); + const utils = api.useUtils(); + const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>( + null, + ); + const [editorTarget, setEditorTarget] = useState(""); - useEffect(() => { - const resolveParams = async () => { - const resolved = await params; - setResolvedParams(resolved); - }; - void resolveParams(); - }, [params]); - - const { data: study } = api.studies.get.useQuery( - { id: resolvedParams?.id ?? "" }, - { enabled: !!resolvedParams?.id }, - ); - - const { data: activeConsentForm, refetch: refetchConsentForm } = - api.studies.getActiveConsentForm.useQuery( - { studyId: resolvedParams?.id ?? "" }, - { enabled: !!resolvedParams?.id }, - ); - - // Only sync once when form loads to avoid resetting user edits - useEffect(() => { - if (activeConsentForm && !editorTarget) { - setEditorTarget(activeConsentForm.content); - } - }, [activeConsentForm, editorTarget]); - - const editor = useEditor({ - extensions: [ - StarterKit, - Table.configure({ - resizable: true, - }), - TableRow, - TableHeader, - TableCell, - Markdown.configure({ - transformPastedText: true, - }), - ], - content: editorTarget || '', - immediatelyRender: false, - onUpdate: ({ editor }) => { - // @ts-ignore - setEditorTarget(editor.storage.markdown.getMarkdown()); - }, - }); - - // Sync Tiptap when editorTarget is set (e.g., from DB) but make sure not to overwrite active edits - useEffect(() => { - if (editor && editorTarget && editor.isEmpty) { - editor.commands.setContent(editorTarget); - } - }, [editorTarget, editor]); - - const generateConsentMutation = api.studies.generateConsentForm.useMutation({ - onSuccess: (data) => { - toast.success("Default Consent Form Generated!"); - setEditorTarget(data.content); - editor?.commands.setContent(data.content); - void refetchConsentForm(); - void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" }); - }, - onError: (error) => { - toast.error("Error generating consent form", { description: error.message }); - }, - }); - - const updateConsentMutation = api.studies.updateConsentForm.useMutation({ - onSuccess: () => { - toast.success("Consent Form Saved Successfully!"); - void refetchConsentForm(); - void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" }); - }, - onError: (error) => { - toast.error("Error saving consent form", { description: error.message }); - }, - }); - - const handleDownloadConsent = async () => { - if (!activeConsentForm || !study || !editor) return; - - try { - toast.loading("Generating Document...", { id: "pdf-gen" }); - await downloadPdfFromHtml(editor.getHTML(), { - filename: `Consent_Form_${study.name.replace(/\s+/g, "_")}_v${activeConsentForm.version}.pdf` - }); - toast.success("Document Downloaded Successfully!", { id: "pdf-gen" }); - } catch (error) { - toast.error("Error generating PDF", { id: "pdf-gen" }); - console.error(error); - } + useEffect(() => { + const resolveParams = async () => { + const resolved = await params; + setResolvedParams(resolved); }; + void resolveParams(); + }, [params]); - useBreadcrumbsEffect([ - { label: "Dashboard", href: "/dashboard" }, - { label: "Studies", href: "/studies" }, - { label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` }, - { label: "Forms" }, - ]); + const { data: study } = api.studies.get.useQuery( + { id: resolvedParams?.id ?? "" }, + { enabled: !!resolvedParams?.id }, + ); - if (!session?.user) { - return notFound(); - } - - if (!study) return
Loading...
; - - return ( - - - -
- - - {activeConsentForm && ( - - )} -
- } - > - {activeConsentForm ? ( -
-
-
-

- {activeConsentForm.title} -

-

- v{activeConsentForm.version} • Status: Active -

-
-
- - Active -
-
- -
-
-
- -
-
- -
-
-
-
- ) : ( - - )} - -
- + const { data: activeConsentForm, refetch: refetchConsentForm } = + api.studies.getActiveConsentForm.useQuery( + { studyId: resolvedParams?.id ?? "" }, + { enabled: !!resolvedParams?.id }, ); + + // Only sync once when form loads to avoid resetting user edits + useEffect(() => { + if (activeConsentForm && !editorTarget) { + setEditorTarget(activeConsentForm.content); + } + }, [activeConsentForm, editorTarget]); + + const editor = useEditor({ + extensions: [ + StarterKit, + Table.configure({ + resizable: true, + }), + TableRow, + TableHeader, + TableCell, + Markdown.configure({ + transformPastedText: true, + }), + ], + content: editorTarget || "", + immediatelyRender: false, + onUpdate: ({ editor }) => { + // @ts-ignore + setEditorTarget(editor.storage.markdown.getMarkdown()); + }, + }); + + // Sync Tiptap when editorTarget is set (e.g., from DB) but make sure not to overwrite active edits + useEffect(() => { + if (editor && editorTarget && editor.isEmpty) { + editor.commands.setContent(editorTarget); + } + }, [editorTarget, editor]); + + const generateConsentMutation = api.studies.generateConsentForm.useMutation({ + onSuccess: (data) => { + toast.success("Default Consent Form Generated!"); + setEditorTarget(data.content); + editor?.commands.setContent(data.content); + void refetchConsentForm(); + void utils.studies.getActivity.invalidate({ + studyId: resolvedParams?.id ?? "", + }); + }, + onError: (error) => { + toast.error("Error generating consent form", { + description: error.message, + }); + }, + }); + + const updateConsentMutation = api.studies.updateConsentForm.useMutation({ + onSuccess: () => { + toast.success("Consent Form Saved Successfully!"); + void refetchConsentForm(); + void utils.studies.getActivity.invalidate({ + studyId: resolvedParams?.id ?? "", + }); + }, + onError: (error) => { + toast.error("Error saving consent form", { description: error.message }); + }, + }); + + const handleDownloadConsent = async () => { + if (!activeConsentForm || !study || !editor) return; + + try { + toast.loading("Generating Document...", { id: "pdf-gen" }); + await downloadPdfFromHtml(editor.getHTML(), { + filename: `Consent_Form_${study.name.replace(/\s+/g, "_")}_v${activeConsentForm.version}.pdf`, + }); + toast.success("Document Downloaded Successfully!", { id: "pdf-gen" }); + } catch (error) { + toast.error("Error generating PDF", { id: "pdf-gen" }); + console.error(error); + } + }; + + useBreadcrumbsEffect([ + { label: "Dashboard", href: "/dashboard" }, + { label: "Studies", href: "/studies" }, + { label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` }, + { label: "Forms" }, + ]); + + if (!session?.user) { + return notFound(); + } + + if (!study) return
Loading...
; + + return ( + + + +
+ + + {activeConsentForm && ( + + )} +
+ } + > + {activeConsentForm ? ( +
+
+
+

+ {activeConsentForm.title} +

+

+ v{activeConsentForm.version} • Status: Active +

+
+
+ + + Active + +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ ) : ( + + )} + +
+ + ); } diff --git a/src/app/(dashboard)/studies/[id]/page.tsx b/src/app/(dashboard)/studies/[id]/page.tsx index aebac74..074dd81 100755 --- a/src/app/(dashboard)/studies/[id]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/page.tsx @@ -273,12 +273,13 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) { {experiment.status} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 706ce09..c433574 100755 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -9,7 +9,12 @@ import { } from "~/lib/storage/minio"; import { auth } from "~/server/auth"; import { db } from "~/server/db"; -import { experiments, mediaCaptures, studyMembers, trials } from "~/server/db/schema"; +import { + experiments, + mediaCaptures, + studyMembers, + trials, +} from "~/server/db/schema"; const uploadSchema = z.object({ trialId: z.string().optional(), @@ -91,15 +96,15 @@ export async function POST(request: NextRequest) { .where( and( eq(studyMembers.studyId, trial[0].studyId), - eq(studyMembers.userId, session.user.id) - ) + eq(studyMembers.userId, session.user.id), + ), ) .limit(1); if (!membership.length) { return NextResponse.json( { error: "Insufficient permissions to upload to this trial" }, - { status: 403 } + { status: 403 }, ); } } diff --git a/src/components/dashboard/app-sidebar.tsx b/src/components/dashboard/app-sidebar.tsx index 9a3b82c..210e675 100755 --- a/src/components/dashboard/app-sidebar.tsx +++ b/src/components/dashboard/app-sidebar.tsx @@ -197,9 +197,9 @@ export function AppSidebar({ // Build study work items with proper URLs when study is selected const studyWorkItemsWithUrls = selectedStudyId ? studyWorkItems.map((item) => ({ - ...item, - url: `/studies/${selectedStudyId}${item.url}`, - })) + ...item, + url: `/studies/${selectedStudyId}${item.url}`, + })) : []; const handleSignOut = async () => { diff --git a/src/components/participants/DigitalSignatureModal.tsx b/src/components/participants/DigitalSignatureModal.tsx index 05d58ec..2cb6eb4 100644 --- a/src/components/participants/DigitalSignatureModal.tsx +++ b/src/components/participants/DigitalSignatureModal.tsx @@ -4,12 +4,12 @@ import { useRef, useState } from "react"; import SignatureCanvas from "react-signature-canvas"; import { Button } from "~/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, } from "~/components/ui/dialog"; import { PenBox, Eraser, Loader2, CheckCircle } from "lucide-react"; import { api } from "~/trpc/react"; @@ -25,211 +25,250 @@ import TableHeader from "@tiptap/extension-table-header"; import { ScrollArea } from "~/components/ui/scroll-area"; interface DigitalSignatureModalProps { - studyId: string; - participantId: string; - participantName?: string | null; - participantCode: string; - activeForm: { id: string; content: string; version: number }; - onSuccess: () => void; + studyId: string; + participantId: string; + participantName?: string | null; + participantCode: string; + activeForm: { id: string; content: string; version: number }; + onSuccess: () => void; } export function DigitalSignatureModal({ - studyId, - participantId, - participantName, - participantCode, - activeForm, - onSuccess, + studyId, + participantId, + participantName, + participantCode, + activeForm, + onSuccess, }: DigitalSignatureModalProps) { - const [isOpen, setIsOpen] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const sigCanvas = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const sigCanvas = useRef(null); - // Mutations - const getUploadUrlMutation = api.participants.getConsentUploadUrl.useMutation(); - const recordConsentMutation = api.participants.recordConsent.useMutation(); + // Mutations + const getUploadUrlMutation = + api.participants.getConsentUploadUrl.useMutation(); + const recordConsentMutation = api.participants.recordConsent.useMutation(); - // Create a preview version of the text - let previewMd = activeForm.content; - previewMd = previewMd.replace(/{{PARTICIPANT_NAME}}/g, participantName ?? "_________________"); - previewMd = previewMd.replace(/{{PARTICIPANT_CODE}}/g, participantCode); - const today = new Date().toLocaleDateString(); - previewMd = previewMd.replace(/{{DATE}}/g, today); - previewMd = previewMd.replace(/{{SIGNATURE_IMAGE}}/g, "_[Signature Here]_"); + // Create a preview version of the text + let previewMd = activeForm.content; + previewMd = previewMd.replace( + /{{PARTICIPANT_NAME}}/g, + participantName ?? "_________________", + ); + previewMd = previewMd.replace(/{{PARTICIPANT_CODE}}/g, participantCode); + const today = new Date().toLocaleDateString(); + previewMd = previewMd.replace(/{{DATE}}/g, today); + previewMd = previewMd.replace(/{{SIGNATURE_IMAGE}}/g, "_[Signature Here]_"); - const previewEditor = useEditor({ - extensions: [StarterKit, Table, TableRow, TableHeader, TableCell, Markdown], - content: previewMd, - editable: false, - immediatelyRender: false, - }); + const previewEditor = useEditor({ + extensions: [StarterKit, Table, TableRow, TableHeader, TableCell, Markdown], + content: previewMd, + editable: false, + immediatelyRender: false, + }); - const handleClear = () => { - sigCanvas.current?.clear(); - }; + const handleClear = () => { + sigCanvas.current?.clear(); + }; - const handleSubmit = async () => { - if (sigCanvas.current?.isEmpty()) { - toast.error("Signature required", { description: "Please sign the document before submitting." }); - return; - } + const handleSubmit = async () => { + if (sigCanvas.current?.isEmpty()) { + toast.error("Signature required", { + description: "Please sign the document before submitting.", + }); + return; + } - try { - setIsSubmitting(true); - toast.loading("Generating Signed Document...", { id: "sig-upload" }); + try { + setIsSubmitting(true); + toast.loading("Generating Signed Document...", { id: "sig-upload" }); - // 1. Get Signature Image Data URL - const signatureDataUrl = sigCanvas.current.getTrimmedCanvas().toDataURL("image/png"); + // 1. Get Signature Image Data URL + const signatureDataUrl = sigCanvas.current + .getTrimmedCanvas() + .toDataURL("image/png"); - // 2. Prepare final Markdown and HTML - let finalMd = activeForm.content; - finalMd = finalMd.replace(/{{PARTICIPANT_NAME}}/g, participantName ?? "_________________"); - finalMd = finalMd.replace(/{{PARTICIPANT_CODE}}/g, participantCode); - finalMd = finalMd.replace(/{{DATE}}/g, today); - finalMd = finalMd.replace(/{{SIGNATURE_IMAGE}}/g, ``); + // 2. Prepare final Markdown and HTML + let finalMd = activeForm.content; + finalMd = finalMd.replace( + /{{PARTICIPANT_NAME}}/g, + participantName ?? "_________________", + ); + finalMd = finalMd.replace(/{{PARTICIPANT_CODE}}/g, participantCode); + finalMd = finalMd.replace(/{{DATE}}/g, today); + finalMd = finalMd.replace( + /{{SIGNATURE_IMAGE}}/g, + ``, + ); - const headlessEditor = new Editor({ - extensions: [StarterKit, Table, TableRow, TableHeader, TableCell, Markdown], - content: finalMd, - }); - const htmlContent = headlessEditor.getHTML(); - headlessEditor.destroy(); + const headlessEditor = new Editor({ + extensions: [ + StarterKit, + Table, + TableRow, + TableHeader, + TableCell, + Markdown, + ], + content: finalMd, + }); + const htmlContent = headlessEditor.getHTML(); + headlessEditor.destroy(); - // 3. Generate PDF Blob - const filename = `Signed_Consent_${participantCode}_v${activeForm.version}.pdf`; - const pdfBlob = await generatePdfBlobFromHtml(htmlContent, { filename }); - const file = new File([pdfBlob], filename, { type: "application/pdf" }); + // 3. Generate PDF Blob + const filename = `Signed_Consent_${participantCode}_v${activeForm.version}.pdf`; + const pdfBlob = await generatePdfBlobFromHtml(htmlContent, { filename }); + const file = new File([pdfBlob], filename, { type: "application/pdf" }); - // 4. Get Presigned URL - toast.loading("Uploading Document...", { id: "sig-upload" }); - const { url, key } = await getUploadUrlMutation.mutateAsync({ - studyId, - participantId, - filename: file.name, - contentType: file.type, - size: file.size, - }); + // 4. Get Presigned URL + toast.loading("Uploading Document...", { id: "sig-upload" }); + const { url, key } = await getUploadUrlMutation.mutateAsync({ + studyId, + participantId, + filename: file.name, + contentType: file.type, + size: file.size, + }); - // 5. Upload to MinIO - await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open("PUT", url, true); - xhr.setRequestHeader("Content-Type", file.type); - 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); - }); + // 5. Upload to MinIO + await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("PUT", url, true); + xhr.setRequestHeader("Content-Type", file.type); + 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); + }); - // 6. Record Consent in DB - toast.loading("Finalizing Consent...", { id: "sig-upload" }); - await recordConsentMutation.mutateAsync({ - participantId, - consentFormId: activeForm.id, - storagePath: key, - }); + // 6. Record Consent in DB + toast.loading("Finalizing Consent...", { id: "sig-upload" }); + await recordConsentMutation.mutateAsync({ + participantId, + consentFormId: activeForm.id, + storagePath: key, + }); - toast.success("Consent Successfully Recorded!", { id: "sig-upload" }); - setIsOpen(false); - onSuccess(); - } catch (error) { - console.error(error); - toast.error("Failed to submit digital signature", { - id: "sig-upload", - description: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Consent Successfully Recorded!", { id: "sig-upload" }); + setIsOpen(false); + onSuccess(); + } catch (error) { + console.error(error); + toast.error("Failed to submit digital signature", { + id: "sig-upload", + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setIsSubmitting(false); + } + }; - return ( - - - - - - - Digital Consent Signature - - Please review the document below and provide your digital signature to consent to this study. - - + return ( + + + + + + + Digital Consent Signature + + Please review the document below and provide your digital signature + to consent to this study. + + -
- {/* Document Preview (Left) */} -
-
- Document Preview -
- -
- -
-
-
+
+ {/* Document Preview (Left) */} +
+
+ Document Preview +
+ +
+ +
+
+
- {/* Signature Panel (Right) */} -
-
-
- Digital Signature Pad -
-
-
- -
-
- -
-

- Draw your signature using your mouse or touch screen inside the box above. -

-
-
- -
- - {/* Submission Actions */} -
-

- - Agreement -

-

- By clicking "Submit Signed Document", you confirm that you have read and understood the information provided in the document preview, and you voluntarily agree to participate in this study. -

- -
-
+ {/* Signature Panel (Right) */} +
+
+
+ Digital Signature Pad +
+
+
+
- -
- ); +
+ +
+

+ Draw your signature using your mouse or touch screen inside + the box above. +

+
+
+ +
+ + {/* Submission Actions */} +
+

+ + Agreement +

+

+ By clicking "Submit Signed Document", you confirm that you have + read and understood the information provided in the document + preview, and you voluntarily agree to participate in this study. +

+ +
+
+
+ + + ); } diff --git a/src/components/participants/ParticipantConsentManager.tsx b/src/components/participants/ParticipantConsentManager.tsx index 6ad356b..b01a78e 100644 --- a/src/components/participants/ParticipantConsentManager.tsx +++ b/src/components/participants/ParticipantConsentManager.tsx @@ -56,7 +56,10 @@ export function ParticipantConsentManager({ existingConsent, participantName, participantCode, -}: ParticipantConsentManagerProps & { participantName?: string | null; participantCode: string }) { +}: ParticipantConsentManagerProps & { + participantName?: string | null; + participantCode: string; +}) { const [isOpen, setIsOpen] = useState(false); const utils = api.useUtils(); @@ -99,14 +102,24 @@ export function ParticipantConsentManager({ // Substitute placeholders in markdown let customMd = activeForm.content; - customMd = customMd.replace(/{{PARTICIPANT_NAME}}/g, participantName ?? "_________________"); + customMd = customMd.replace( + /{{PARTICIPANT_NAME}}/g, + participantName ?? "_________________", + ); customMd = customMd.replace(/{{PARTICIPANT_CODE}}/g, participantCode); customMd = customMd.replace(/{{DATE}}/g, "_________________"); customMd = customMd.replace(/{{SIGNATURE_IMAGE}}/g, ""); // Blank ready for physical signature // Use headless Tiptap to parse MD to HTML via same extensions const editor = new Editor({ - extensions: [StarterKit, Table, TableRow, TableHeader, TableCell, Markdown], + extensions: [ + StarterKit, + Table, + TableRow, + TableHeader, + TableCell, + Markdown, + ], content: customMd, }); @@ -195,7 +208,11 @@ export function ParticipantConsentManager({ activeForm={activeForm} onSuccess={handleSuccess} /> - diff --git a/src/components/participants/ParticipantForm.tsx b/src/components/participants/ParticipantForm.tsx index 8dc6f61..b664e66 100755 --- a/src/components/participants/ParticipantForm.tsx +++ b/src/components/participants/ParticipantForm.tsx @@ -119,39 +119,39 @@ export function ParticipantForm({ { label: "Studies", href: "/studies" }, ...(contextStudyId ? [ - { - label: participant?.study?.name ?? "Study", - href: `/studies/${contextStudyId}`, - }, - { - label: "Participants", - href: `/studies/${contextStudyId}/participants`, - }, - ...(mode === "edit" && participant - ? [ - { - label: participant.name ?? participant.participantCode, - href: `/studies/${contextStudyId}/participants/${participant.id}`, - }, - { label: "Edit" }, - ] - : [{ label: "New Participant" }]), - ] + { + label: participant?.study?.name ?? "Study", + href: `/studies/${contextStudyId}`, + }, + { + label: "Participants", + href: `/studies/${contextStudyId}/participants`, + }, + ...(mode === "edit" && participant + ? [ + { + label: participant.name ?? participant.participantCode, + href: `/studies/${contextStudyId}/participants/${participant.id}`, + }, + { label: "Edit" }, + ] + : [{ label: "New Participant" }]), + ] : [ - { - label: "Participants", - href: `/studies/${contextStudyId}/participants`, - }, - ...(mode === "edit" && participant - ? [ - { - label: participant.name ?? participant.participantCode, - href: `/studies/${contextStudyId}/participants/${participant.id}`, - }, - { label: "Edit" }, - ] - : [{ label: "New Participant" }]), - ]), + { + label: "Participants", + href: `/studies/${contextStudyId}/participants`, + }, + ...(mode === "edit" && participant + ? [ + { + label: participant.name ?? participant.participantCode, + href: `/studies/${contextStudyId}/participants/${participant.id}`, + }, + { label: "Edit" }, + ] + : [{ label: "New Participant" }]), + ]), ]; useBreadcrumbsEffect(breadcrumbs); @@ -291,7 +291,7 @@ export function ParticipantForm({ readOnly={true} className={cn( "bg-muted text-muted-foreground", - form.formState.errors.participantCode ? "border-red-500" : "" + form.formState.errors.participantCode ? "border-red-500" : "", )} /> {form.formState.errors.participantCode && ( @@ -338,7 +338,11 @@ export function ParticipantForm({
{!contextStudyId && ( @@ -358,7 +362,9 @@ export function ParticipantForm({ } > @@ -404,11 +410,11 @@ export function ParticipantForm({ form.setValue( "gender", value as - | "male" - | "female" - | "non_binary" - | "prefer_not_to_say" - | "other", + | "male" + | "female" + | "non_binary" + | "prefer_not_to_say" + | "other", ) } > diff --git a/src/components/trials/wizard/WizardInterface.tsx b/src/components/trials/wizard/WizardInterface.tsx index 8088833..ce9b3ae 100755 --- a/src/components/trials/wizard/WizardInterface.tsx +++ b/src/components/trials/wizard/WizardInterface.tsx @@ -187,7 +187,8 @@ export const WizardInterface = React.memo(function WizardInterface({ }, }); - const executeSystemActionMutation = api.robots.executeSystemAction.useMutation(); + const executeSystemActionMutation = + api.robots.executeSystemAction.useMutation(); const [isCompleting, setIsCompleting] = useState(false); // Map database step types to component step types @@ -632,17 +633,19 @@ export const WizardInterface = React.memo(function WizardInterface({ if (matchedOption) { // Handle both string options and object options for nextStepId - const nextStepId = typeof matchedOption === "string" - ? null // String options don't have nextStepId - : matchedOption.nextStepId; + const nextStepId = + typeof matchedOption === "string" + ? null // String options don't have nextStepId + : matchedOption.nextStepId; if (nextStepId) { // Find index of the target step const targetIndex = steps.findIndex((s) => s.id === nextStepId); if (targetIndex !== -1) { - const label = typeof matchedOption === "string" - ? matchedOption - : matchedOption.label; + const label = + typeof matchedOption === "string" + ? matchedOption + : matchedOption.label; console.log( `[WizardInterface] Branching to step ${targetIndex} (${label})`, diff --git a/src/hooks/useWizardRos.ts b/src/hooks/useWizardRos.ts index 8939af2..cfc452c 100644 --- a/src/hooks/useWizardRos.ts +++ b/src/hooks/useWizardRos.ts @@ -345,7 +345,8 @@ export function useWizardRos( ...execution, status: "failed", endTime: new Date(), - error: error instanceof Error ? error.message : "System action failed", + error: + error instanceof Error ? error.message : "System action failed", }; service.emit("action_failed", failedExecution); throw error; diff --git a/src/lib/pdf-generator.ts b/src/lib/pdf-generator.ts index 3628b8b..31bd7d2 100644 --- a/src/lib/pdf-generator.ts +++ b/src/lib/pdf-generator.ts @@ -1,61 +1,76 @@ export interface PdfOptions { - filename?: string; + filename?: string; } const getHtml2PdfOptions = (filename?: string) => ({ - margin: 0.5, - filename: filename ?? 'document.pdf', - image: { type: 'jpeg' as const, quality: 0.98 }, - html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff", windowWidth: 800 }, - jsPDF: { unit: 'in', format: 'letter' as const, orientation: 'portrait' as const } + margin: 0.5, + filename: filename ?? "document.pdf", + image: { type: "jpeg" as const, quality: 0.98 }, + html2canvas: { + scale: 2, + useCORS: true, + backgroundColor: "#ffffff", + windowWidth: 800, + }, + jsPDF: { + unit: "in", + format: "letter" as const, + orientation: "portrait" as const, + }, }); const createPrintWrapper = (htmlContent: string) => { - const printWrapper = document.createElement("div"); - printWrapper.style.position = "absolute"; - printWrapper.style.left = "-9999px"; - printWrapper.style.top = "0px"; - printWrapper.className = "light"; // Prevent dark mode variables from bleeding into the physical PDF + const printWrapper = document.createElement("div"); + printWrapper.style.position = "absolute"; + printWrapper.style.left = "-9999px"; + printWrapper.style.top = "0px"; + printWrapper.className = "light"; // Prevent dark mode variables from bleeding into the physical PDF - const element = document.createElement("div"); - element.innerHTML = htmlContent; - // Assign standard prose layout and explicitly white/black print colors - element.className = "prose prose-sm max-w-none p-12 bg-white text-black"; - element.style.width = "800px"; - element.style.backgroundColor = "white"; - element.style.color = "black"; + const element = document.createElement("div"); + element.innerHTML = htmlContent; + // Assign standard prose layout and explicitly white/black print colors + element.className = "prose prose-sm max-w-none p-12 bg-white text-black"; + element.style.width = "800px"; + element.style.backgroundColor = "white"; + element.style.color = "black"; - printWrapper.appendChild(element); - document.body.appendChild(printWrapper); + printWrapper.appendChild(element); + document.body.appendChild(printWrapper); - return { printWrapper, element }; + return { printWrapper, element }; }; -export async function downloadPdfFromHtml(htmlContent: string, options: PdfOptions = {}): Promise { - // @ts-ignore - Dynamic import to prevent SSR issues with window/document - const html2pdf = (await import('html2pdf.js')).default; +export async function downloadPdfFromHtml( + htmlContent: string, + options: PdfOptions = {}, +): Promise { + // @ts-ignore - Dynamic import to prevent SSR issues with window/document + const html2pdf = (await import("html2pdf.js")).default; - const { printWrapper, element } = createPrintWrapper(htmlContent); + const { printWrapper, element } = createPrintWrapper(htmlContent); - try { - const opt = getHtml2PdfOptions(options.filename); - await html2pdf().set(opt).from(element).save(); - } finally { - document.body.removeChild(printWrapper); - } + try { + const opt = getHtml2PdfOptions(options.filename); + await html2pdf().set(opt).from(element).save(); + } finally { + document.body.removeChild(printWrapper); + } } -export async function generatePdfBlobFromHtml(htmlContent: string, options: PdfOptions = {}): Promise { - // @ts-ignore - Dynamic import to prevent SSR issues with window/document - const html2pdf = (await import('html2pdf.js')).default; +export async function generatePdfBlobFromHtml( + htmlContent: string, + options: PdfOptions = {}, +): Promise { + // @ts-ignore - Dynamic import to prevent SSR issues with window/document + const html2pdf = (await import("html2pdf.js")).default; - const { printWrapper, element } = createPrintWrapper(htmlContent); + const { printWrapper, element } = createPrintWrapper(htmlContent); - try { - const opt = getHtml2PdfOptions(options.filename); - const pdfBlob = await html2pdf().set(opt).from(element).output('blob'); - return pdfBlob; - } finally { - document.body.removeChild(printWrapper); - } + try { + const opt = getHtml2PdfOptions(options.filename); + const pdfBlob = await html2pdf().set(opt).from(element).output("blob"); + return pdfBlob; + } finally { + document.body.removeChild(printWrapper); + } } diff --git a/src/server/api/routers/robots.ts b/src/server/api/routers/robots.ts index 13b624a..4aefee0 100755 --- a/src/server/api/routers/robots.ts +++ b/src/server/api/routers/robots.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { exec } from "child_process"; import { promisify } from "util"; - + const execAsync = promisify(exec); import type { db } from "~/server/db"; import { @@ -491,7 +491,7 @@ export const robotsRouter = createTRPCRouter({ return installedPlugins; }), - + initialize: protectedProcedure .input( z.object({ @@ -499,27 +499,31 @@ export const robotsRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - const robotIp = process.env.NAO_ROBOT_IP || process.env.NAO_IP || "134.82.159.168"; + const robotIp = + process.env.NAO_ROBOT_IP || process.env.NAO_IP || "134.82.159.168"; const password = process.env.NAO_PASSWORD || "robolab"; - + console.log(`[Robots] Initializing robot ${input.id} at ${robotIp}`); - + try { // 1. Disable Autonomous Life const disableAlCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; al = naoqi.ALProxy('ALAutonomousLife', '127.0.0.1', 9559); al.setState('disabled')\\""`; - + // 2. Wake Up (Stand Up) const wakeUpCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.wakeUp()\\""`; - + // Execute commands sequentially console.log("[Robots] Executing AL disable..."); await execAsync(disableAlCmd).catch((e) => - console.warn("AL disable failed (non-critical/already disabled):", e), + console.warn( + "AL disable failed (non-critical/already disabled):", + e, + ), ); - + console.log("[Robots] Executing Wake Up..."); await execAsync(wakeUpCmd); - + return { success: true }; } catch (error) { console.error("Robot initialization failed:", error); @@ -529,7 +533,7 @@ export const robotsRouter = createTRPCRouter({ }); } }), - + executeSystemAction: protectedProcedure .input( z.object({ @@ -538,14 +542,15 @@ export const robotsRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - const robotIp = process.env.NAO_ROBOT_IP || process.env.NAO_IP || "134.82.159.168"; + const robotIp = + process.env.NAO_ROBOT_IP || process.env.NAO_IP || "134.82.159.168"; const password = process.env.NAO_PASSWORD || "robolab"; - + console.log(`[Robots] Executing system action ${input.id}`); - + try { let command = ""; - + switch (input.id) { case "say_with_emotion": case "say_text_with_emotion": { @@ -560,23 +565,23 @@ export const robotsRouter = createTRPCRouter({ : emotion === "thinking" ? "^thoughtful" : "^joyful"; - + command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; s = naoqi.ALProxy('ALAnimatedSpeech', '127.0.0.1', 9559); s.say('${tag} ${text.replace(/'/g, "\\'")}')\\""`; break; } - + case "wake_up": command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.wakeUp()\\""`; break; - + case "rest": command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.rest()\\""`; break; - + default: throw new Error(`System action ${input.id} not implemented`); } - + await execAsync(command); return { success: true }; } catch (error) { diff --git a/src/server/services/robot-communication.ts b/src/server/services/robot-communication.ts index a2bcb4c..1746b22 100755 --- a/src/server/services/robot-communication.ts +++ b/src/server/services/robot-communication.ts @@ -188,7 +188,7 @@ export class RobotCommunicationService extends EventEmitter { console.log(`[RobotComm] Executing robot action: ${action.actionId}`); console.log(`[RobotComm] Topic: ${action.implementation.topic}`); console.log(`[RobotComm] Parameters:`, action.parameters); - + // Execute action based on type and platform this.executeRobotActionInternal(action, actionId); } catch (error) { diff --git a/src/server/services/trial-execution.ts b/src/server/services/trial-execution.ts index 410fda8..2b9476a 100755 --- a/src/server/services/trial-execution.ts +++ b/src/server/services/trial-execution.ts @@ -668,7 +668,7 @@ export class TrialExecutionEngine { .from(plugins) .where(eq(plugins.identifier, pluginName)) .limit(1); - + if (byIdentifier) { plugin = byIdentifier; } else {