diff --git a/bun.lock b/bun.lock old mode 100755 new mode 100644 diff --git a/src/app/(dashboard)/nao-test/page.tsx b/src/app/(dashboard)/nao-test/page.tsx index 498f8a8..6896ccf 100755 --- a/src/app/(dashboard)/nao-test/page.tsx +++ b/src/app/(dashboard)/nao-test/page.tsx @@ -64,7 +64,8 @@ export default function NaoTestPage() { const [sensorData, setSensorData] = useState({}); const logsEndRef = useRef(null); - const ROS_BRIDGE_URL = "ws://134.82.159.25:9090"; + const ROS_BRIDGE_URL = + process.env.NEXT_PUBLIC_ROS_BRIDGE_URL || "ws://localhost:9090"; const addLog = (message: string) => { const timestamp = new Date().toLocaleTimeString(); diff --git a/src/app/(dashboard)/experiments/[id]/designer/DesignerPageClient.tsx b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx similarity index 94% rename from src/app/(dashboard)/experiments/[id]/designer/DesignerPageClient.tsx rename to src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx index 00f2db8..aae68a5 100755 --- a/src/app/(dashboard)/experiments/[id]/designer/DesignerPageClient.tsx +++ b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx @@ -48,7 +48,7 @@ export function DesignerPageClient({ }, { label: experiment.name, - href: `/experiments/${experiment.id}`, + href: `/studies/${experiment.study.id}/experiments/${experiment.id}`, }, { label: "Designer", diff --git a/src/app/(dashboard)/experiments/[id]/designer/page.tsx b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx similarity index 97% rename from src/app/(dashboard)/experiments/[id]/designer/page.tsx rename to src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx index e4705bb..08093d8 100755 --- a/src/app/(dashboard)/experiments/[id]/designer/page.tsx +++ b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/page.tsx @@ -11,7 +11,7 @@ import { DesignerPageClient } from "./DesignerPageClient"; interface ExperimentDesignerPageProps { params: Promise<{ - id: string; + experimentId: string; }>; } @@ -20,7 +20,7 @@ export default async function ExperimentDesignerPage({ }: ExperimentDesignerPageProps) { try { const resolvedParams = await params; - const experiment = await api.experiments.get({ id: resolvedParams.id }); + const experiment = await api.experiments.get({ id: resolvedParams.experimentId }); if (!experiment) { notFound(); @@ -36,13 +36,13 @@ export default async function ExperimentDesignerPage({ // Only pass initialDesign if there's existing visual design data let initialDesign: | { - id: string; - name: string; - description: string; - steps: ExperimentStep[]; - version: number; - lastSaved: Date; - } + id: string; + name: string; + description: string; + steps: ExperimentStep[]; + version: number; + lastSaved: Date; + } | undefined; if (existingDesign?.steps && existingDesign.steps.length > 0) { @@ -258,7 +258,7 @@ export async function generateMetadata({ }> { try { const resolvedParams = await params; - const experiment = await api.experiments.get({ id: resolvedParams.id }); + const experiment = await api.experiments.get({ id: resolvedParams.experimentId }); return { title: `${experiment?.name} - Designer | HRIStudio`, diff --git a/src/app/(dashboard)/studies/[id]/page.tsx b/src/app/(dashboard)/studies/[id]/page.tsx index edf17ae..8df523c 100755 --- a/src/app/(dashboard)/studies/[id]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/page.tsx @@ -185,7 +185,7 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) { @@ -263,20 +263,19 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {

{experiment.name}

{experiment.status} @@ -300,12 +299,12 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
diff --git a/src/components/experiments/ExperimentForm.tsx b/src/components/experiments/ExperimentForm.tsx index d25fe69..fcf2d6e 100755 --- a/src/components/experiments/ExperimentForm.tsx +++ b/src/components/experiments/ExperimentForm.tsx @@ -87,33 +87,33 @@ export function ExperimentForm({ mode, experimentId }: ExperimentFormProps) { { label: "Studies", href: "/studies" }, ...(selectedStudyId ? [ - { - label: experiment?.study?.name ?? "Study", - href: `/studies/${selectedStudyId}`, - }, - { label: "Experiments", href: "/experiments" }, - ...(mode === "edit" && experiment - ? [ - { - label: experiment.name, - href: `/experiments/${experiment.id}`, - }, - { label: "Edit" }, - ] - : [{ label: "New Experiment" }]), - ] + { + label: experiment?.study?.name ?? "Study", + href: `/studies/${selectedStudyId}`, + }, + { label: "Experiments", href: "/experiments" }, + ...(mode === "edit" && experiment + ? [ + { + label: experiment.name, + href: `/studies/${selectedStudyId}/experiments/${experiment.id}`, + }, + { label: "Edit" }, + ] + : [{ label: "New Experiment" }]), + ] : [ - { label: "Experiments", href: "/experiments" }, - ...(mode === "edit" && experiment - ? [ - { - label: experiment.name, - href: `/experiments/${experiment.id}`, - }, - { label: "Edit" }, - ] - : [{ label: "New Experiment" }]), - ]), + { label: "Experiments", href: "/experiments" }, + ...(mode === "edit" && experiment + ? [ + { + label: experiment.name, + href: `/studies/${experiment.studyId}/experiments/${experiment.id}`, + }, + { label: "Edit" }, + ] + : [{ label: "New Experiment" }]), + ]), ]; useBreadcrumbsEffect(breadcrumbs); @@ -153,14 +153,14 @@ export function ExperimentForm({ mode, experimentId }: ExperimentFormProps) { ...data, estimatedDuration: data.estimatedDuration ?? undefined, }); - router.push(`/experiments/${newExperiment.id}/designer`); + router.push(`/studies/${data.studyId}/experiments/${newExperiment.id}/designer`); } else { const updatedExperiment = await updateExperimentMutation.mutateAsync({ id: experimentId!, ...data, estimatedDuration: data.estimatedDuration ?? undefined, }); - router.push(`/experiments/${updatedExperiment.id}`); + router.push(`/studies/${experiment?.studyId ?? data.studyId}/experiments/${updatedExperiment.id}`); } } catch (error) { setError( diff --git a/src/components/experiments/ExperimentsGrid.tsx b/src/components/experiments/ExperimentsGrid.tsx index 5c75de8..90154fc 100755 --- a/src/components/experiments/ExperimentsGrid.tsx +++ b/src/components/experiments/ExperimentsGrid.tsx @@ -78,7 +78,7 @@ function ExperimentCard({ experiment }: ExperimentCardProps) {
{experiment.name} @@ -158,10 +158,10 @@ function ExperimentCard({ experiment }: ExperimentCardProps) { {/* Actions */}
+ +
+ ); + return ( -
+
- - -
- } + actions={actions} /> -
- toggleLibraryScrollLock(false)} +
+ {/* Loading Overlay */} + {!isReady && ( +
+
+ +

Loading designer...

+
+
+ )} + + {/* Main Content - Fade in when ready */} +
- - -
- } - center={} - right={ -
- -
- } - /> - - {dragOverlayAction ? ( -
- {dragOverlayAction.name} -
- ) : null} -
- -
- persist()} - onValidate={() => validateDesign()} - onExport={() => handleExport()} - lastSavedAt={lastSavedAt} - saving={isSaving} - validating={isValidating} - exporting={isExporting} - /> +
+ toggleLibraryScrollLock(false)} + > + + +
+ } + center={} + right={ +
+ +
+ } + /> + + {dragOverlayAction ? ( +
+ {dragOverlayAction.name} +
+ ) : null} +
+ +
+ persist()} + onValidate={() => validateDesign()} + onExport={() => handleExport()} + onRecalculateHash={() => recomputeHash()} + lastSavedAt={lastSavedAt} + saving={isSaving} + validating={isValidating} + exporting={isExporting} + /> +
+
diff --git a/src/components/experiments/designer/PropertiesPanel.tsx b/src/components/experiments/designer/PropertiesPanel.tsx index 20ad8fa..9736be8 100755 --- a/src/components/experiments/designer/PropertiesPanel.tsx +++ b/src/components/experiments/designer/PropertiesPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { @@ -80,6 +80,85 @@ export function PropertiesPanel({ }: PropertiesPanelProps) { const registry = actionRegistry; + // Local state for controlled inputs + const [localActionName, setLocalActionName] = useState(""); + const [localStepName, setLocalStepName] = useState(""); + const [localStepDescription, setLocalStepDescription] = useState(""); + const [localParams, setLocalParams] = useState>({}); + + // Debounce timers + const actionUpdateTimer = useRef(undefined); + const stepUpdateTimer = useRef(undefined); + const paramUpdateTimers = useRef(new Map()); + + // Sync local state when selection ID changes (not on every object recreation) + useEffect(() => { + if (selectedAction) { + setLocalActionName(selectedAction.name); + setLocalParams(selectedAction.parameters); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedAction?.id]); + + useEffect(() => { + if (selectedStep) { + setLocalStepName(selectedStep.name); + setLocalStepDescription(selectedStep.description ?? ""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedStep?.id]); + + // Cleanup timers on unmount + useEffect(() => { + const timersMap = paramUpdateTimers.current; + return () => { + if (actionUpdateTimer.current) clearTimeout(actionUpdateTimer.current); + if (stepUpdateTimer.current) clearTimeout(stepUpdateTimer.current); + timersMap.forEach((timer) => clearTimeout(timer)); + }; + }, []); + + // Debounced update handlers + const debouncedActionUpdate = useCallback( + (stepId: string, actionId: string, updates: Partial) => { + if (actionUpdateTimer.current) clearTimeout(actionUpdateTimer.current); + actionUpdateTimer.current = setTimeout(() => { + onActionUpdate(stepId, actionId, updates); + }, 300); + }, + [onActionUpdate], + ); + + const debouncedStepUpdate = useCallback( + (stepId: string, updates: Partial) => { + if (stepUpdateTimer.current) clearTimeout(stepUpdateTimer.current); + stepUpdateTimer.current = setTimeout(() => { + onStepUpdate(stepId, updates); + }, 300); + }, + [onStepUpdate], + ); + + const debouncedParamUpdate = useCallback( + (stepId: string, actionId: string, paramId: string, value: unknown) => { + const existing = paramUpdateTimers.current.get(paramId); + if (existing) clearTimeout(existing); + + const timer = setTimeout(() => { + onActionUpdate(stepId, actionId, { + parameters: { + ...selectedAction?.parameters, + [paramId]: value, + }, + }); + paramUpdateTimers.current.delete(paramId); + }, 300); + + paramUpdateTimers.current.set(paramId, timer); + }, + [onActionUpdate, selectedAction?.parameters], + ); + // Find containing step for selected action (if any) const containingStep = selectedAction && @@ -176,12 +255,21 @@ export function PropertiesPanel({
- onActionUpdate(containingStep.id, selectedAction.id, { - name: e.target.value, - }) - } + value={localActionName} + onChange={(e) => { + const newName = e.target.value; + setLocalActionName(newName); + debouncedActionUpdate(containingStep.id, selectedAction.id, { + name: newName, + }); + }} + onBlur={() => { + if (localActionName !== selectedAction.name) { + onActionUpdate(containingStep.id, selectedAction.id, { + name: localActionName, + }); + } + }} className="mt-1 h-7 w-full text-xs" />
@@ -210,6 +298,17 @@ export function PropertiesPanel({ /* ---- Handlers ---- */ const updateParamValue = (value: unknown) => { + setLocalParams((prev) => ({ ...prev, [param.id]: value })); + debouncedParamUpdate( + containingStep.id, + selectedAction.id, + param.id, + value, + ); + }; + + const updateParamValueImmediate = (value: unknown) => { + setLocalParams((prev) => ({ ...prev, [param.id]: value })); onActionUpdate(containingStep.id, selectedAction.id, { parameters: { ...selectedAction.parameters, @@ -218,23 +317,50 @@ export function PropertiesPanel({ }); }; + const updateParamLocal = (value: unknown) => { + setLocalParams((prev) => ({ ...prev, [param.id]: value })); + }; + + const commitParamValue = () => { + if (localParams[param.id] !== rawValue) { + onActionUpdate(containingStep.id, selectedAction.id, { + parameters: { + ...selectedAction.parameters, + [param.id]: localParams[param.id], + }, + }); + } + }; + /* ---- Control Rendering ---- */ let control: React.ReactNode = null; if (param.type === "text") { + const localValue = localParams[param.id] ?? rawValue ?? ""; control = ( updateParamValue(e.target.value)} + onBlur={() => { + if (localParams[param.id] !== rawValue) { + onActionUpdate(containingStep.id, selectedAction.id, { + parameters: { + ...selectedAction.parameters, + [param.id]: localParams[param.id], + }, + }); + } + }} className="mt-1 h-7 w-full text-xs" /> ); } else if (param.type === "select") { + const localValue = localParams[param.id] ?? rawValue ?? ""; control = ( ); } else if (param.type === "boolean") { + const localValue = localParams[param.id] ?? rawValue ?? false; control = (
updateParamValue(val)} + checked={Boolean(localValue)} + onCheckedChange={(val) => + updateParamValueImmediate(val) + } aria-label={param.name} /> - {Boolean(rawValue) ? "Enabled" : "Disabled"} + {Boolean(localValue) ? "Enabled" : "Disabled"}
); } else if (param.type === "number") { + const localValue = localParams[param.id] ?? rawValue; const numericVal = - typeof rawValue === "number" - ? rawValue + typeof localValue === "number" + ? localValue : typeof param.value === "number" ? param.value : (param.min ?? 0); @@ -295,8 +425,9 @@ export function PropertiesPanel({ step={step} value={[Number(numericVal)]} onValueChange={(vals: number[]) => - updateParamValue(vals[0]) + updateParamLocal(vals[0]) } + onPointerUp={commitParamValue} /> {step < 1 @@ -318,6 +449,20 @@ export function PropertiesPanel({ onChange={(e) => updateParamValue(parseFloat(e.target.value) || 0) } + onBlur={() => { + if (localParams[param.id] !== rawValue) { + onActionUpdate( + containingStep.id, + selectedAction.id, + { + parameters: { + ...selectedAction.parameters, + [param.id]: localParams[param.id], + }, + }, + ); + } + }} className="mt-1 h-7 w-full text-xs" /> ); @@ -373,23 +518,41 @@ export function PropertiesPanel({
- onStepUpdate(selectedStep.id, { name: e.target.value }) - } + value={localStepName} + onChange={(e) => { + const newName = e.target.value; + setLocalStepName(newName); + debouncedStepUpdate(selectedStep.id, { name: newName }); + }} + onBlur={() => { + if (localStepName !== selectedStep.name) { + onStepUpdate(selectedStep.id, { name: localStepName }); + } + }} className="mt-1 h-7 w-full text-xs" />
- onStepUpdate(selectedStep.id, { - description: e.target.value, - }) - } + onChange={(e) => { + const newDesc = e.target.value; + setLocalStepDescription(newDesc); + debouncedStepUpdate(selectedStep.id, { + description: newDesc, + }); + }} + onBlur={() => { + if ( + localStepDescription !== (selectedStep.description ?? "") + ) { + onStepUpdate(selectedStep.id, { + description: localStepDescription, + }); + } + }} className="mt-1 h-7 w-full text-xs" />
@@ -405,9 +568,9 @@ export function PropertiesPanel({ + onValueChange={(val) => { onStepUpdate(selectedStep.id, { trigger: { ...selectedStep.trigger, type: val as TriggerType, }, - }) - } + }); + }} > diff --git a/src/components/experiments/designer/flow/FlowWorkspace.tsx b/src/components/experiments/designer/flow/FlowWorkspace.tsx index 5e9ae41..6154f8b 100755 --- a/src/components/experiments/designer/flow/FlowWorkspace.tsx +++ b/src/components/experiments/designer/flow/FlowWorkspace.tsx @@ -111,7 +111,7 @@ function StepDroppableArea({ stepId }: { stepId: string }) { className={cn( "pointer-events-none absolute inset-0 rounded-md transition-colors", isOver && - "bg-blue-50/40 ring-2 ring-blue-400/60 ring-offset-0 dark:bg-blue-950/20", + "bg-blue-50/40 ring-2 ring-blue-400/60 ring-offset-0 dark:bg-blue-950/20", )} /> ); @@ -182,11 +182,11 @@ function SortableActionChip({ "h-2.5 w-2.5 rounded-full", def ? { - wizard: "bg-blue-500", - robot: "bg-emerald-500", - control: "bg-amber-500", - observation: "bg-purple-500", - }[def.category] + wizard: "bg-blue-500", + robot: "bg-emerald-500", + control: "bg-amber-500", + observation: "bg-purple-500", + }[def.category] : "bg-slate-400", )} /> @@ -608,7 +608,7 @@ export function FlowWorkspace({ renameStep( step, (e.target as HTMLInputElement).value.trim() || - step.name, + step.name, ); setRenamingStepId(null); void recomputeHash(); @@ -777,4 +777,6 @@ export function FlowWorkspace({ ); } -export default FlowWorkspace; +// Wrap in React.memo to prevent unnecessary re-renders causing flashing +export default React.memo(FlowWorkspace); + diff --git a/src/components/experiments/designer/layout/BottomStatusBar.tsx b/src/components/experiments/designer/layout/BottomStatusBar.tsx index 1d517d4..2a998ef 100755 --- a/src/components/experiments/designer/layout/BottomStatusBar.tsx +++ b/src/components/experiments/designer/layout/BottomStatusBar.tsx @@ -40,7 +40,7 @@ export interface BottomStatusBarProps { onValidate?: () => void; onExport?: () => void; onOpenCommandPalette?: () => void; - onToggleVersionStrategy?: () => void; + onRecalculateHash?: () => void; className?: string; saving?: boolean; validating?: boolean; @@ -56,7 +56,7 @@ export function BottomStatusBar({ onValidate, onExport, onOpenCommandPalette, - onToggleVersionStrategy, + onRecalculateHash, className, saving, validating, @@ -198,9 +198,9 @@ export function BottomStatusBar({ if (onOpenCommandPalette) onOpenCommandPalette(); }, [onOpenCommandPalette]); - const handleToggleVersionStrategy = useCallback(() => { - if (onToggleVersionStrategy) onToggleVersionStrategy(); - }, [onToggleVersionStrategy]); + const handleRecalculateHash = useCallback(() => { + if (onRecalculateHash) onRecalculateHash(); + }, [onRecalculateHash]); /* ------------------------------------------------------------------------ */ /* Render */ @@ -265,12 +265,21 @@ export function BottomStatusBar({ {autoSaveEnabled ? "auto-save on" : "auto-save off"}
- - {versionStrategy.replace(/_/g, " ")} + + {currentDesignHash?.slice(0, 16) ?? 'โ€”'} +
void; /** - * Whether to auto-switch to properties tab when selection changes. + * If true, auto-switch to "properties" when a selection occurs. */ autoFocusOnSelection?: boolean; + /** + * Study plugins with name and metadata + */ + studyPlugins?: Array<{ + id: string; + robotId: string; + name: string; + version: string; + }>; } export function InspectorPanel({ @@ -58,6 +67,7 @@ export function InspectorPanel({ activeTab, onTabChange, autoFocusOnSelection = true, + studyPlugins, }: InspectorPanelProps) { /* ------------------------------------------------------------------------ */ /* Store Selectors */ @@ -339,6 +349,7 @@ export function InspectorPanel({ steps={steps} actionSignatureDrift={actionSignatureDrift} actionDefinitions={actionRegistry.getAllActions()} + studyPlugins={studyPlugins} onReconcileAction={(actionId) => { // Placeholder: future diff modal / signature update diff --git a/src/components/experiments/designer/state/hashing.ts b/src/components/experiments/designer/state/hashing.ts index f536103..7c4a50e 100755 --- a/src/components/experiments/designer/state/hashing.ts +++ b/src/components/experiments/designer/state/hashing.ts @@ -130,7 +130,7 @@ export interface DesignHashOptions { } const DEFAULT_OPTIONS: Required = { - includeParameterValues: false, + includeParameterValues: true, // Changed to true so parameter changes trigger hash updates includeActionNames: true, includeStepNames: true, }; @@ -175,16 +175,16 @@ function projectExecutionDescriptor( timeoutMs: exec.timeoutMs ?? null, ros2: exec.ros2 ? { - topic: exec.ros2.topic ?? null, - service: exec.ros2.service ?? null, - action: exec.ros2.action ?? null, - } + topic: exec.ros2.topic ?? null, + service: exec.ros2.service ?? null, + action: exec.ros2.action ?? null, + } : null, rest: exec.rest ? { - method: exec.rest.method, - path: exec.rest.path, - } + method: exec.rest.method, + path: exec.rest.path, + } : null, }; } @@ -244,10 +244,10 @@ export async function computeActionSignature( baseActionId: def.baseActionId ?? null, execution: def.execution ? { - transport: def.execution.transport, - retryable: def.execution.retryable ?? false, - timeoutMs: def.execution.timeoutMs ?? null, - } + transport: def.execution.transport, + retryable: def.execution.retryable ?? false, + timeoutMs: def.execution.timeoutMs ?? null, + } : null, schema: def.parameterSchemaRaw ? canonicalize(def.parameterSchemaRaw) : null, }; @@ -301,7 +301,12 @@ export async function computeIncrementalDesignHash( // First compute per-action hashes for (const step of steps) { for (const action of step.actions) { - const existing = previous?.actionHashes.get(action.id); + // Only reuse cached hash if we're NOT including parameter values + // (because parameter values can change without changing the action ID) + const existing = !options.includeParameterValues + ? previous?.actionHashes.get(action.id) + : undefined; + if (existing) { // Simple heuristic: if shallow structural keys unchanged, reuse // (We still project to confirm minimal structure; deeper diff omitted for performance.) @@ -316,7 +321,12 @@ export async function computeIncrementalDesignHash( // Then compute step hashes (including ordered list of action hashes) for (const step of steps) { - const existing = previous?.stepHashes.get(step.id); + // Only reuse cached hash if we're NOT including parameter values + // (because parameter values in actions can change without changing the step ID) + const existing = !options.includeParameterValues + ? previous?.stepHashes.get(step.id) + : undefined; + if (existing) { stepHashes.set(step.id, existing); continue; diff --git a/src/components/experiments/experiments-columns.tsx b/src/components/experiments/experiments-columns.tsx index b8c7be7..89d858f 100755 --- a/src/components/experiments/experiments-columns.tsx +++ b/src/components/experiments/experiments-columns.tsx @@ -114,14 +114,14 @@ function ExperimentActionsCell({ experiment }: { experiment: Experiment }) { - + View Details - + Open Designer @@ -129,7 +129,7 @@ function ExperimentActionsCell({ experiment }: { experiment: Experiment }) { {experiment.canEdit && ( - + Edit Experiment @@ -202,7 +202,7 @@ export const experimentsColumns: ColumnDef[] = [ return (
diff --git a/src/lib/nao6-transforms.ts b/src/lib/nao6-transforms.ts index e5a7db1..6728776 100755 --- a/src/lib/nao6-transforms.ts +++ b/src/lib/nao6-transforms.ts @@ -70,7 +70,7 @@ export function getCameraImage( ): Record { const camera = params.camera as string; const topic = - camera === "front" ? "/camera/front/image_raw" : "/camera/bottom/image_raw"; + camera === "front" ? "/naoqi_driver/camera/front/image_raw" : "/naoqi_driver/camera/bottom/image_raw"; return { subscribe: true, @@ -88,7 +88,7 @@ export function getJointStates( ): Record { return { subscribe: true, - topic: "/joint_states", + topic: "/naoqi_driver/joint_states", messageType: "sensor_msgs/msg/JointState", once: true, }; @@ -102,7 +102,7 @@ export function getImuData( ): Record { return { subscribe: true, - topic: "/imu/torso", + topic: "/naoqi_driver/imu/torso", messageType: "sensor_msgs/msg/Imu", once: true, }; @@ -116,7 +116,7 @@ export function getBumperStatus( ): Record { return { subscribe: true, - topic: "/bumper", + topic: "/naoqi_driver/bumper", messageType: "naoqi_bridge_msgs/msg/Bumper", once: true, }; @@ -129,7 +129,7 @@ export function getTouchSensors( params: Record, ): Record { const sensorType = params.sensor_type as string; - const topic = sensorType === "hand" ? "/hand_touch" : "/head_touch"; + const topic = sensorType === "hand" ? "/naoqi_driver/hand_touch" : "/naoqi_driver/head_touch"; const messageType = sensorType === "hand" ? "naoqi_bridge_msgs/msg/HandTouch" @@ -153,12 +153,12 @@ export function getSonarRange( let topic: string; if (sensor === "left") { - topic = "/sonar/left"; + topic = "/naoqi_driver/sonar/left"; } else if (sensor === "right") { - topic = "/sonar/right"; + topic = "/naoqi_driver/sonar/right"; } else { // For "both", we'll default to left and let the wizard interface handle multiple calls - topic = "/sonar/left"; + topic = "/naoqi_driver/sonar/left"; } return { @@ -177,7 +177,7 @@ export function getRobotInfo( ): Record { return { subscribe: true, - topic: "/info", + topic: "/naoqi_driver/info", messageType: "naoqi_bridge_msgs/msg/RobotInfo", once: true, }; diff --git a/src/server/api/routers/experiments.ts b/src/server/api/routers/experiments.ts index 16e1574..1a3ef0c 100755 --- a/src/server/api/routers/experiments.ts +++ b/src/server/api/routers/experiments.ts @@ -152,10 +152,10 @@ export const experimentsRouter = createTRPCRouter({ .select({ experimentId: trials.experimentId, latest: sql`max(GREATEST( - COALESCE(${trials.completedAt}, 'epoch'::timestamptz), - COALESCE(${trials.startedAt}, 'epoch'::timestamptz), - COALESCE(${trials.createdAt}, 'epoch'::timestamptz) - ))`.as("latest"), + COALESCE(${trials.completedAt}, 'epoch':: timestamptz), + COALESCE(${trials.startedAt}, 'epoch':: timestamptz), + COALESCE(${trials.createdAt}, 'epoch':: timestamptz) +))`.as("latest"), }) .from(trials) .where(inArray(trials.experimentId, experimentIds)) @@ -360,24 +360,24 @@ export const experimentsRouter = createTRPCRouter({ const executionGraphSummary = stepsArray ? { - steps: stepsArray.length, - actions: stepsArray.reduce((total, step) => { - const acts = step.actions; - return ( - total + - (Array.isArray(acts) - ? acts.reduce( - (aTotal, a) => - aTotal + - (Array.isArray(a?.actions) ? a.actions.length : 0), - 0, - ) - : 0) - ); - }, 0), - generatedAt: eg?.generatedAt ?? null, - version: eg?.version ?? null, - } + steps: stepsArray.length, + actions: stepsArray.reduce((total, step) => { + const acts = step.actions; + return ( + total + + (Array.isArray(acts) + ? acts.reduce( + (aTotal, a) => + aTotal + + (Array.isArray(a?.actions) ? a.actions.length : 0), + 0, + ) + : 0) + ); + }, 0), + generatedAt: eg?.generatedAt ?? null, + version: eg?.version ?? null, + } : null; return { @@ -511,8 +511,7 @@ export const experimentsRouter = createTRPCRouter({ return { valid: false, issues: [ - `Compilation failed: ${ - err instanceof Error ? err.message : "Unknown error" + `Compilation failed: ${err instanceof Error ? err.message : "Unknown error" }`, ], pluginDependencies: [], @@ -541,13 +540,13 @@ export const experimentsRouter = createTRPCRouter({ integrityHash: compiledGraph?.hash ?? null, compiled: compiledGraph ? { - steps: compiledGraph.steps.length, - actions: compiledGraph.steps.reduce( - (acc, s) => acc + s.actions.length, - 0, - ), - transportSummary: summarizeTransports(compiledGraph.steps), - } + steps: compiledGraph.steps.length, + actions: compiledGraph.steps.reduce( + (acc, s) => acc + s.actions.length, + 0, + ), + transportSummary: summarizeTransports(compiledGraph.steps), + } : null, }; }), @@ -570,6 +569,7 @@ export const experimentsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const { id, createSteps, compileExecution, ...updateData } = input; const userId = ctx.session.user.id; + console.log("[DEBUG] experiments.update called", { id, visualDesign: updateData.visualDesign, createSteps }); // Get experiment to check study access const experiment = await ctx.db.query.experiments.findFirst({ @@ -607,7 +607,7 @@ export const experimentsRouter = createTRPCRouter({ if (issues.length) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Visual design validation failed:\n- ${issues.join("\n- ")}`, + message: `Visual design validation failed: \n - ${issues.join("\n- ")}`, }); } normalizedSteps = guardedSteps; @@ -637,11 +637,10 @@ export const experimentsRouter = createTRPCRouter({ } catch (compileErr) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Execution graph compilation failed: ${ - compileErr instanceof Error - ? compileErr.message - : "Unknown error" - }`, + message: `Execution graph compilation failed: ${compileErr instanceof Error + ? compileErr.message + : "Unknown error" + }`, }); } } @@ -735,11 +734,13 @@ export const experimentsRouter = createTRPCRouter({ const updatedExperiment = updatedExperimentResults[0]; if (!updatedExperiment) { + console.error("[DEBUG] Failed to update experiment - no result returned"); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update experiment", }); } + console.log("[DEBUG] Experiment updated successfully", { updatedAt: updatedExperiment.updatedAt }); // Log activity await ctx.db.insert(activityLogs).values({