File size: 3,766 Bytes
2a8cee5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useRef, useState } from 'react';
import { useAppContext } from '../utils/app.context';
import StorageUtils from '../utils/storage';
import { useNavigate } from 'react-router';
import ChatMessage from './ChatMessage';
import { PendingMessage } from '../utils/types';

export default function ChatScreen() {
  const {
    viewingConversation,
    sendMessage,
    isGenerating,
    stopGenerating,
    pendingMessages,
  } = useAppContext();
  const [inputMsg, setInputMsg] = useState('');
  const containerRef = useRef<HTMLDivElement>(null);
  const navigate = useNavigate();

  const currConvId = viewingConversation?.id ?? '';
  const pendingMsg: PendingMessage | undefined = pendingMessages[currConvId];

  const scrollToBottom = (requiresNearBottom: boolean) => {
    if (!containerRef.current) return;
    const msgListElem = containerRef.current;
    const spaceToBottom =
      msgListElem.scrollHeight -
      msgListElem.scrollTop -
      msgListElem.clientHeight;
    if (!requiresNearBottom || spaceToBottom < 50) {
      setTimeout(
        () => msgListElem.scrollTo({ top: msgListElem.scrollHeight }),
        1
      );
    }
  };

  // scroll to bottom when conversation changes
  useEffect(() => {
    scrollToBottom(false);
  }, [viewingConversation?.id]);

  const sendNewMessage = async () => {
    if (inputMsg.trim().length === 0 || isGenerating(currConvId)) return;
    const convId = viewingConversation?.id ?? StorageUtils.getNewConvId();
    const lastInpMsg = inputMsg;
    setInputMsg('');
    if (!viewingConversation) {
      // if user is creating a new conversation, redirect to the new conversation
      navigate(`/chat/${convId}`);
    }
    scrollToBottom(false);
    // auto scroll as message is being generated
    const onChunk = () => scrollToBottom(true);
    if (!(await sendMessage(convId, inputMsg, onChunk))) {
      // restore the input message if failed
      setInputMsg(lastInpMsg);
    }
  };

  return (
    <>
      {/* chat messages */}
      <div
        id="messages-list"
        className="flex flex-col grow overflow-y-auto"
        ref={containerRef}
      >
        <div className="mt-auto flex justify-center">
          {/* placeholder to shift the message to the bottom */}
          {viewingConversation ? '' : 'Send a message to start'}
        </div>
        {viewingConversation?.messages.map((msg) => (
          <ChatMessage key={msg.id} msg={msg} scrollToBottom={scrollToBottom} />
        ))}

        {pendingMsg && (
          <ChatMessage
            msg={pendingMsg}
            scrollToBottom={scrollToBottom}
            isPending
            id="pending-msg"
          />
        )}
      </div>

      {/* chat input */}
      <div className="flex flex-row items-center mt-8 mb-6">
        <textarea
          className="textarea textarea-bordered w-full"
          placeholder="Type a message (Shift+Enter to add a new line)"
          value={inputMsg}
          onChange={(e) => setInputMsg(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && e.shiftKey) return;
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault();
              sendNewMessage();
            }
          }}
          id="msg-input"
          dir="auto"
        ></textarea>
        {isGenerating(currConvId) ? (
          <button
            className="btn btn-neutral ml-2"
            onClick={() => stopGenerating(currConvId)}
          >
            Stop
          </button>
        ) : (
          <button
            className="btn btn-primary ml-2"
            onClick={sendNewMessage}
            disabled={inputMsg.trim().length === 0}
          >
            Send
          </button>
        )}
      </div>
    </>
  );
}