import React, { useState, useEffect, useRef } from 'react'; import { Send, SkipForward } from 'lucide-react'; /** * Inline input slot rendered in the chat stream when it's the human * participant's turn. Replaces what would otherwise be the LLM's * message bubble. Visually distinct via a thick green left-edge * accent and a pulsing border so it's obvious "we're waiting on you". * * Props: * awaiting - the awaiting_human payload from the most recent * human_turn_needed SSE event: * { speaker_id, speaker_name, phase, * addressed_to?, asker_name?, prompt_context? } * onSubmit - async (text) => void * onSkip - async () => void (only when allowSkip) * allowSkip - bool, defaults true * sending - bool: disable the buttons while a submit is in flight */ export default function HumanInputSlot({ awaiting, onSubmit, onSkip, allowSkip = true, sending = false, }) { const [text, setText] = useState(''); const taRef = useRef(null); // Auto-focus when the slot first appears, so the user can start // typing immediately without hunting for the textarea. useEffect(() => { if (awaiting && taRef.current) { taRef.current.focus(); } // We intentionally narrow the dep list to the identity of the // pending turn (speaker + phase). Re-focusing on every awaiting // mutation would steal focus from the user mid-type. // eslint-disable-next-line react-hooks/exhaustive-deps }, [awaiting?.speaker_id, awaiting?.phase]); if (!awaiting) return null; const name = awaiting.speaker_name || 'you'; const askerLine = awaiting.asker_name ? `${awaiting.asker_name} asked: ${awaiting.prompt_context || '…'}` : awaiting.prompt_context || ''; const handleSubmit = async () => { const value = text.trim(); if (!value) return; await onSubmit?.(value); setText(''); }; const handleKeyDown = (e) => { // Ctrl/Cmd+Enter submits; plain Enter inserts a newline (textarea default). if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); handleSubmit(); } }; return (
{name}
{askerLine && (
{askerLine}
)}