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

Code before:
  /** Chat messages data. Requires cyclic buffer. */
  data: Rec[]
  /** Chat input box placeholder. Use for prompt examples. */
  placeholder?: S
}

export const XChatbot = ({ model }: { model: Chatbot }) => {
  const
    [msgs, setMsgs] = React.useState<ChatMessage[]>([]),
    [userInput, setUserInput] = React.useState(''),
    msgContainerRef = React.useRef<HTMLDivElement>(null),
    onChange = (e: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newVal = '') => {
      e.preventDefault()
      wave.args[model.name] = newVal
      setUserInput(newVal)
    },
    onKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { if (e.key === 'Enter') submit() },
    submit = () => {
      if (!userInput.trim()) return
      setMsgs([...msgs, { msg: userInput, fromUser: true }])
      wave.push()
      setUserInput('')
    }

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

  return (
    <div className={css.chatWindow}>
      <div className={css.msgContainer} ref={msgContainerRef}>
        {msgs.map(({ msg, fromUser }, idx) => (
          <div
            key={idx}
            className={clas(css.msgWrapper, fromUser ? css.userMsg : '')}
            style={{
              paddingTop: msgs[idx - 1]?.fromUser !== fromUser ? rem(0.7) : 0,
              paddingBottom: msgs?.[idx + 1]?.fromUser !== fromUser ? rem(0.85) : 0,
              color: cssVar(fromUser ? '$neutralDark' : '$themePrimary')
            }} >
            <div
              className={clas(css.msg, 'wave-s14')}
              style={{ padding: msg?.includes('\n') ? 12 : 6 }}>
              <span className={css.text}>
                <Markdown source={msg || ''} />
              </span>
            </div>
          </div>
        ))}
      </div>

Review comment (suggestion):
ScrollTo is only [supported](https://caniuse.com/?search=scrollTo) on Safari (fully) & Edge from 2020, let's add a fallback to the previous `scrollTop` in case it's not available.

Code after:
  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.key === 'Enter') submit() },
    submit = () => {
      if (!userInput.trim()) return
      setMsgs([...msgs, { msg: userInput, fromUser: true }])
      wave.push()
      setUserInput('')
    }

  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 className={css.msgContainer} ref={msgContainerRef}>
        {msgs.map(({ msg, fromUser }, idx) => (
          <div
            key={idx}
            className={clas(css.msgWrapper, fromUser ? '' : css.botMsg)}
            style={{
              paddingTop: msgs[idx - 1]?.fromUser !== fromUser ? rem(0.8) : 0,
              paddingBottom: msgs?.[idx + 1]?.fromUser !== fromUser ? rem(0.8) : 0,
              color: fromUser ? '$text' : botTextColor
            }} >
            <span
              className={clas(css.msg, 'wave-s14')}
              style={{ padding: msg?.includes('\n') ? 12 : 6 }}>
              <Markdown source={msg || ''} />
            </span>
          </div>
        ))}
      </div>
      <div className={css.textInput}>
        <Fluent.TextField
          data-test={model.name}
          value={userInput}