Custom User Input Scroll Wheel & Voice not sending variable to next block

I am not a coder, I asked Gemini to create code for a custom scroll wheel for the user to select among different diseases, or they can speak into the microphone. I want it to output to a variable “input_cat” and then the Router block will use AI to parse it into one of several discrete categories for further parsing. It gets stuck and does not pass to the next block even with the Continue button pressed. I have unfortunately wasted four hours on this, and I am stuck :slight_smile:

import React, { useState, useEffect, useRef } from ‘react’;

export default function CustomPickerWheel({ json, status, onUpdate, onComplete, onSubmit }) {

const options = [

"Trigeminal Neuralgia",

"Hemifacial Spasm",

"Acoustic Neuroma",

"Pituitary Adenoma",

"Skull Base Meningioma",

"Minimally Invasive Endoscopic Surgery",

"Gamma Knife Radiosurgery",

"Research Pathways",

"YouTube Video Guides",

"Patient Reviews"

];

const [selectedIndex, setSelectedIndex] = useState(0);

const [isListening, setIsListening] = useState(false);

const [transcript, setTranscript] = useState(“”);

const [hasSpoken, setHasSpoken] = useState(false);

const scrollContainerRef = useRef(null);

const audioContextRef = useRef(null);

const recognitionRef = useRef(null);

const scrollTimeoutRef = useRef(null);

const isUpdatingRef = useRef(false);

const ITEM_HEIGHT = 64;

const VIEWPORT_HEIGHT = 400;

const initAudio = () => {

if (!audioContextRef.current) {

  audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();

}

};

const playClickSound = () => {

try {

  initAudio();

  if (!audioContextRef.current) return;

  if (audioContextRef.current.state === 'suspended') {

    audioContextRef.current.resume();

  }

  const ctx = audioContextRef.current;

  const osc = ctx.createOscillator();

  const gain = ctx.createGain();



  osc.type = 'sine';

  osc.frequency.setValueAtTime(800, ctx.currentTime); 

  gain.gain.setValueAtTime(0.08, ctx.currentTime);     

  gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.03); 



  osc.connect(gain);

  gain.connect(ctx.destination);

  osc.start();

  osc.stop(ctx.currentTime + 0.03);

} catch (e) {

  console.warn("Audio feedback block failed:", e);

}

};

useEffect(() => {

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (SpeechRecognition) {

  const rec = new SpeechRecognition();

  rec.continuous = false;

  rec.interimResults = false;

  rec.lang = 'en-US';



  rec.onstart = () => {

    setIsListening(true);

    setHasSpoken(true);

  };



  rec.onresult = (event) => {

    const speechToText = event.results\[0\]\[0\].transcript;

    setTranscript(speechToText);

    

    let matchedOption = options\[0\];

    const lowerSpeech = speechToText.toLowerCase();

    for (let opt of options) {

      if (lowerSpeech.includes(opt.toLowerCase())) {

        matchedOption = opt;

        break;

      }

    }



    const targetIdx = options.indexOf(matchedOption);

    if (targetIdx !== -1) {

      setSelectedIndex(targetIdx);

      if (scrollContainerRef.current) {

        scrollContainerRef.current.scrollTo({

          top: targetIdx \* ITEM_HEIGHT,

          behavior: 'smooth'

        });

      }

    }



    if (onUpdate) {

      onUpdate({ input_cat: matchedOption });

    }

  };



  rec.onerror = () => {

    setIsListening(false);

    setTranscript("Could not hear clearly. Try again.");

  };



  rec.onend = () => { setIsListening(false); };

  recognitionRef.current = rec;

}



return () => {

  if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);

};

}, []);

const updateToIndex = (nextIndex) => {

if (nextIndex < 0 || nextIndex >= options.length) return;



isUpdatingRef.current = true;

setSelectedIndex(nextIndex);

playClickSound();

setHasSpoken(false);



if (scrollContainerRef.current) {

  scrollContainerRef.current.scrollTo({

    top: nextIndex \* ITEM_HEIGHT,

    behavior: 'smooth'

  });

}



if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);

scrollTimeoutRef.current = setTimeout(() => {

  if (onUpdate) {

    onUpdate({ input_cat: options\[nextIndex\] });

  }

  isUpdatingRef.current = false;

}, 150);

};

const handleWheel = (e) => {

e.preventDefault(); 

const direction = e.deltaY > 0 ? 1 : -1;

const nextIndex = selectedIndex + direction;

updateToIndex(nextIndex);

};

const handleScroll = () => {

if (isUpdatingRef.current) return; 



const container = scrollContainerRef.current;

if (!container) return;



const scrollTop = container.scrollTop;

let index = Math.round(scrollTop / ITEM_HEIGHT);

index = Math.max(0, Math.min(index, options.length - 1));



if (index !== selectedIndex) {

  setSelectedIndex(index);

  playClickSound();

  setHasSpoken(false);



  if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);

  scrollTimeoutRef.current = setTimeout(() => {

    if (onUpdate) {

      onUpdate({ input_cat: options\[index\] });

    }

  }, 150);

}

};

// CRITICAL FIX: Save variable, wait for MindStudio to register state, then apply all possible submission handles.

const handleConfirmSubmission = () => {

let finalChoice = options\[selectedIndex\];



if (transcript && hasSpoken) {

  const lowerSpeech = transcript.toLowerCase();

  for (let opt of options) {

    if (lowerSpeech.includes(opt.toLowerCase())) {

      finalChoice = opt;

      break;

    }

  }

}



// 1. Commit the payload data

if (onUpdate) {

  onUpdate({ input_cat: finalChoice });

}



// 2. Short timeout ensures MindStudio receives data before the view changes

setTimeout(() => {

  if (onComplete) {

    onComplete();

  } else if (onSubmit) {

    onSubmit();

  } else if (window.MindStudio?.onComplete) {

    window.MindStudio.onComplete();

  } else if (window.MindStudio?.onSubmit) {

    window.MindStudio.onSubmit();

  } else {

    // Ultimate fallback for custom iframe structures communicating via postMessage

    window.parent.postMessage({ type: 'MINSTUDIO_COMPLETE', data: { input_cat: finalChoice } }, '\*');

    window.parent.postMessage({ type: 'SUBMIT' }, '\*');

  }

}, 100);

};

return (

<div 

  style={{ width: '94%', maxWidth: '550px', margin: '20px auto', padding: '24px 16px', backgroundColor: '#ffffff', borderRadius: '20px', border: '1px solid #f1f5f9', boxShadow: '0 10px 30px -5px rgba(0,0,0,0.08)', fontFamily: 'system-ui, sans-serif', boxSizing: 'border-box' }}

  onTouchStart={initAudio} 

  onMouseDown={initAudio}

>

  <div style={{ marginBottom: '20px', textAlign: 'center' }}>

    <label style={{ fontSize: '12px', fontWeight: '700', letterSpacing: '0.1em', textTransform: 'uppercase', color: '#94a3b8' }}>

      Select Condition or Topic

    </label>

  </div>



  <div style={{ position: 'relative', height: \`${VIEWPORT_HEIGHT}px\`, width: '100%', overflow: 'hidden', backgroundColor: '#f8fafc', borderRadius: '16px', border: '1px solid #e2e8f0', marginBottom: '20px' }}>

    <div style={{ position: 'absolute', top: '50%', left: '12px', right: '12px', height: \`${ITEM_HEIGHT}px\`, transform: 'translateY(-50%)', pointerEvents: 'none', borderRadius: '10px', zIndex: 10, borderTop: '2px solid rgba(37, 99, 235, 0.1)', borderBottom: '2px solid rgba(37, 99, 235, 0.1)', backgroundColor: 'rgba(219, 234, 254, 0.2)' }} />

    

    <div 

      ref={scrollContainerRef} 

      onScroll={handleScroll}

      onWheel={handleWheel} 

      style={{ height: '100%', width: '100%', overflowY: 'scroll', scrollSnapType: 'y mandatory', scrollbarWidth: 'none', msOverflowStyle: 'none', boxSizing: 'border-box' }} 

      className="no-scrollbar"

    >

      <div style={{ height: \`${(VIEWPORT_HEIGHT - ITEM_HEIGHT) / 2}px\` }} />

      

      {options.map((option, idx) => (

        <div key={idx} style={{ height: \`${ITEM_HEIGHT}px\`, display: 'flex', alignItems: 'center', justifyContent: 'center', scrollSnapAlign: 'center', cursor: 'pointer', padding: '0 24px', boxSizing: 'border-box' }} onClick={() => { initAudio(); updateToIndex(idx); }}>

          <span style={{ textAlign: 'center', width: '100%', display: 'block', wordWrap: 'break-word', whiteSpace: 'normal', lineHeight: '1.2', transition: 'all 0.25s ease-out', transform: idx === selectedIndex ? 'scale(1.1)' : 'scale(0.92)', fontWeight: idx === selectedIndex ? '700' : '500', color: idx === selectedIndex ? '#1e3a8a' : '#94a3b8', opacity: idx === selectedIndex ? 1 : 0.4, fontSize: idx === selectedIndex ? '20px' : '16px' }}>{option}</span>

        </div>

      ))}



      <div style={{ height: \`${(VIEWPORT_HEIGHT - ITEM_HEIGHT) / 2}px\` }} />

    </div>

  </div>



  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '16px' }}>

    <button type="button" onClick={handleConfirmSubmission} style={{ width: '100%', padding: '16px', borderRadius: '12px', border: 'none', backgroundColor: '#1e3a8a', color: '#ffffff', fontSize: '16px', fontWeight: '600', cursor: 'pointer', boxShadow: '0 4px 14px rgba(30,58,138,0.2)' }}>

      Confirm & Continue →

    </button>

    <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>

      <button type="button" onClick={() => { initAudio(); if (recognitionRef.current) { if (isListening) recognitionRef.current.stop(); else { setTranscript(""); recognitionRef.current.start(); } } }} style={{ width: '44px', height: '44px', borderRadius: '50%', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: isListening ? '#ef4444' : '#2563eb' }}>

        {isListening ? <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="1"/></svg> : <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 1v11a4 4 0 0 0 4-4V5a4 4 0 0 0-8 0v3a4 4 0 0 0 4 4z"/><path d="M19 10v1a7 7 0 0 1-14 0v-1"/><line x1="12" y1="19" x2="12" y2="23"/></svg>}

      </button>

      <p style={{ fontSize: '13px', fontWeight: '500', color: isListening ? '#ef4444' : '#64748b', margin: '0' }}>

        {isListening ? "Listening Closely..." : hasSpoken ? \`"${transcript}"\` : "Describe symptoms via microphone"}

      </p>

    </div>

  </div>

</div>

);

}

Hi @bluemarblepa,

Sorry to hear the block is getting stuck. Could you share a remix link so I can take a look at what might be causing it? Here’s how you can get the link:

  1. Click the agent’s name at the top left of the editor
  2. Go to the Remixing tab
  3. Enable remixing and copy the link
  4. Publish the agent

This will let me copy your agent and pinpoint what’s causing the issue.