File size: 4,149 Bytes
11bf9b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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 (
    <div className="ccai-human-slot" data-speaker-id={awaiting.speaker_id || ''}>
      <div className="ccai-human-slot-accent" />
      <div className="ccai-human-slot-body">
        <div className="ccai-human-slot-header">
          <span className="ccai-human-slot-name">{name}</span>
          <span className="ccai-human-slot-pulse" aria-hidden="true" />
          <span className="ccai-human-slot-prompt">
            {name}, please type your response here.
          </span>
        </div>
        {askerLine && (
          <div className="ccai-human-slot-context">{askerLine}</div>
        )}
        <textarea
          ref={taRef}
          className="ccai-human-slot-textarea"
          value={text}
          onChange={e => setText(e.target.value)}
          onKeyDown={handleKeyDown}
          rows={4}
          placeholder={`${name} please type your response here`}
          disabled={sending}
        />
        <div className="ccai-human-slot-actions">
          <span className="ccai-human-slot-hint">
            Ctrl+Enter to submit
          </span>
          <div className="ccai-human-slot-actions-right">
            {allowSkip && (
              <button
                type="button"
                className="btn-sm btn-outline ccai-human-slot-skip"
                onClick={() => onSkip?.()}
                disabled={sending}
                title="Skip my turn this round"
              >
                <SkipForward size={14} style={{ marginRight: 4 }} />
                Skip my turn
              </button>
            )}
            <button
              type="button"
              className="btn btn-primary btn-sm ccai-human-slot-submit"
              onClick={handleSubmit}
              disabled={sending || !text.trim()}
            >
              <Send size={14} style={{ marginRight: 4 }} />
              {sending ? 'Sending…' : 'Submit'}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}