All files / src/pages/Robot Robot.tsx

0% Statements 0/42
0% Branches 0/20
0% Functions 0/16
0% Lines 0/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 121 122 123 124 125 126 127 128 129 130 131                                                                                                                                                                                                                                                                     
import { useState, useEffect, useRef } from 'react'
 
/**
 * Displays a live robot interaction panel with user input, conversation history,
 * and real-time updates from the robot backend via Server-Sent Events (SSE).
 *
 * @returns A React element rendering the interactive robot UI.
 */
export default function Robot() {
  /** The text message currently entered by the user. */
  const [message, setMessage] = useState('');
 
  /** Whether the robot’s microphone or listening mode is currently active. */
  const [listening, setListening] = useState(false);
  /** The ongoing conversation history as a sequence of user/assistant messages. */
  const [conversation, setConversation] = useState<
  {"role": "user" | "assistant", "content": string}[]>([])
  /** Reference to the scrollable conversation container for auto-scrolling. */
  const conversationRef = useRef<HTMLDivElement | null>(null);
  /**
   * Index used to force refresh the SSE connection or clear conversation.
   * Incrementing this value triggers a reset of the live data stream.
   */
  const [conversationIndex, setConversationIndex] = useState(0);
 
  /**
   * Sends a message to the robot backend.
   *
   * Makes a POST request to `/message` with the user’s text.
   * The backend may respond with confirmation or error information.
   */
  const sendMessage = async () => {
    try {
      const response = await fetch("http://localhost:8000/message", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ message }),
      });
      const data = await response.json();
      console.log(data);
    } catch (error) {
      console.error("Error sending message: ", error);
    }
  };
 
   /**
   * Establishes a persistent Server-Sent Events (SSE) connection
   * to receive real-time updates from the robot backend.
   *
   * Handles three event types:
   * - `voice_active`: whether the robot is currently listening.
   * - `speech`: recognized user speech input.
   * - `llm_response`: the robot’s language model-generated reply.
   *
   * The connection resets whenever `conversationIndex` changes.
   */
  useEffect(() => {
    const eventSource = new EventSource("http://localhost:8000/sse");
 
    eventSource.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        if ("voice_active" in data) setListening(data.voice_active);
        if ("speech" in data) setConversation(conversation => [...conversation, {"role": "user", "content": data.speech}]);
        if ("llm_response" in data) setConversation(conversation => [...conversation, {"role": "assistant", "content": data.llm_response}]);
      } catch {
        console.log("Unparsable SSE message:", event.data);
      }
    };
 
    return () => {
      eventSource.close();
    };
  }, [conversationIndex]);
 
  /**
   * Automatically scrolls the conversation view to the bottom
   * whenever a new message is added.
   */
  useEffect(() => {
    if (!conversationRef || !conversationRef.current) return;
    conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
  }, [conversation]);
 
  return (
    <>
      <h1>Robot interaction</h1>
      <h2>Force robot speech</h2>
      <div className={"flex-row gap-md justify-center"}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
          placeholder="Enter a message"
        />
        <button onClick={sendMessage}>Speak</button>
      </div>
      <div className={"flex-col gap-lg align-center"}>
        <h2>Conversation</h2>
        <p>Listening {listening ? "🟢" : "🔴"}</p>
        <div style={{ maxHeight: "200px", maxWidth: "600px", overflowY: "auto"}} ref={conversationRef}>
          {conversation.map((item, i) => (
            <p key={i}
              style={{
                backgroundColor: item["role"] == "user"
                  ? "color-mix(in oklab, canvas, blue 20%)"
                  : "color-mix(in oklab, canvas, gray 20%)",
                whiteSpace: "pre-line",
              }}
              className={"round-md padding-md"}
            >{item["content"]}</p>
          ))}
        </div>
        <div className={"flex-row gap-md justify-center"}>
          <button onClick={() => {
            setConversationIndex((conversationIndex) => conversationIndex + 1)
            setConversation([])
          }}>Reset</button>
          <button onClick={() => {
            setConversationIndex((conversationIndex) => conversationIndex == -1 ? 0 : -1)
            setConversation([])
          }}>{conversationIndex == -1 ? "Start" : "Stop"}</button>
        </div>
      </div>
    </>
  );
}