Repository: h2oai/wave
File: ui/src/chatbot.tsx (TypeScript)

Code before:
  name: Id
  /** Chat messages data. Requires cyclic buffer. */
  data: Rec[]
  /** Chat input box placeholder. Use for prompt examples. */
  placeholder?: S
  /** The events to capture on this chatbot. One of 'stop'. */
  events?: S[]
  /** True to show a button to stop the text generation. Defaults to False. */
  generating?: B
}

export const XChatbot = ({ model }: { model: Chatbot }) => {
  const
    [msgs, setMsgs] = React.useState<ChatMessage[]>([]),
    [userInput, setUserInput] = React.useState(''),
    msgContainerRef = React.useRef<HTMLDivElement>(null),
    theme = Fluent.useTheme(),
    botTextColor = React.useMemo(() => getContrast(theme.palette.neutralLighter), [theme.palette.neutralLighter]),
    onChange = (e: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newVal = '') => {
      e.preventDefault()
      wave.args[model.name] = newVal
      setUserInput(newVal)
    },
    onKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      if (!e.shiftKey && e.key === 'Enter') {
        e.preventDefault() // Prevents a newline from being added to the text box.
        submit()
      }
    },
    submit = () => {
      if (!userInput.trim()) return
      setMsgs([...msgs, { msg: userInput, fromUser: true }])
      wave.push()
      setUserInput('')
    },
    stopGenerating = () => { if (model.events?.includes('stop')) wave.emit(model.name, 'stop', true) }

  React.useEffect(() => {
    if (!msgContainerRef.current) return
    const offsetTop = msgContainerRef.current.scrollHeight
    if (msgContainerRef.current?.scrollTo) msgContainerRef.current.scrollTo({ top: offsetTop, behavior: 'smooth' })
    else msgContainerRef.current.scrollTop = offsetTop
  }, [msgs])
  React.useEffect(() => { if (model.data) setMsgs(model.data as ChatMessage[]) }, [model.data])

  return (
    <div className={css.chatWindow}>
      <div
        ref={msgContainerRef}
        className={css.msgContainer}
        // Height of input box + padding (+ height of stop button).

Review comment (suggestion):
```suggestion
        e.preventDefault() // Do not add an extra newline to the input text box after submission.
```

Code after:
type ChatMessage = { msg: S, fromUser: B }

/** Create a chatbot card to allow getting prompts from users and providing them with LLM generated answers. */
export interface Chatbot {
  /** An identifying name for this component. */
  name: Id
  /** Chat messages data. Requires cyclic buffer. */
  data: Rec[]
  /** Chat input box placeholder. Use for prompt examples. */
  placeholder?: S
  /** The events to capture on this chatbot. One of 'stop'. */
  events?: S[]
  /** True to show a button to stop the text generation. Defaults to False. */
  generating?: B
}

export const XChatbot = ({ model }: { model: Chatbot }) => {
  const
    [msgs, setMsgs] = React.useState<ChatMessage[]>([]),
    [userInput, setUserInput] = React.useState(''),
    msgContainerRef = React.useRef<HTMLDivElement>(null),
    theme = Fluent.useTheme(),
    botTextColor = React.useMemo(() => getContrast(theme.palette.neutralLighter), [theme.palette.neutralLighter]),
    onChange = (e: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newVal = '') => {
      e.preventDefault()
      wave.args[model.name] = newVal
      setUserInput(newVal)
    },
    onKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      if (!e.shiftKey && e.key === 'Enter') {
        e.preventDefault() // Do not add an extra newline to the input text box after submission.
        submit()
      }
    },
    submit = () => {
      if (!userInput.trim()) return
      setMsgs([...msgs, { msg: userInput, fromUser: true }])
      wave.push()
      setUserInput('')
    },
    stopGenerating = () => { if (model.events?.includes('stop')) wave.emit(model.name, 'stop', true) }

  React.useEffect(() => {
    if (!msgContainerRef.current) return
    const offsetTop = msgContainerRef.current.scrollHeight
    if (msgContainerRef.current?.scrollTo) msgContainerRef.current.scrollTo({ top: offsetTop, behavior: 'smooth' })
    else msgContainerRef.current.scrollTop = offsetTop
  }, [msgs])
  React.useEffect(() => { if (model.data) setMsgs(model.data as ChatMessage[]) }, [model.data])

  return (