ChatScreen.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { useEffect, useRef, useState } from 'react';
  2. import { useAppContext } from '../utils/app.context';
  3. import StorageUtils from '../utils/storage';
  4. import { useNavigate } from 'react-router';
  5. import ChatMessage from './ChatMessage';
  6. import { PendingMessage } from '../utils/types';
  7. export default function ChatScreen() {
  8. const {
  9. viewingConversation,
  10. sendMessage,
  11. isGenerating,
  12. stopGenerating,
  13. pendingMessages,
  14. } = useAppContext();
  15. const [inputMsg, setInputMsg] = useState('');
  16. const containerRef = useRef<HTMLDivElement>(null);
  17. const navigate = useNavigate();
  18. const currConvId = viewingConversation?.id ?? '';
  19. const pendingMsg: PendingMessage | undefined = pendingMessages[currConvId];
  20. const scrollToBottom = (requiresNearBottom: boolean) => {
  21. if (!containerRef.current) return;
  22. const msgListElem = containerRef.current;
  23. const spaceToBottom =
  24. msgListElem.scrollHeight -
  25. msgListElem.scrollTop -
  26. msgListElem.clientHeight;
  27. if (!requiresNearBottom || spaceToBottom < 50) {
  28. setTimeout(
  29. () => msgListElem.scrollTo({ top: msgListElem.scrollHeight }),
  30. 1
  31. );
  32. }
  33. };
  34. // scroll to bottom when conversation changes
  35. useEffect(() => {
  36. scrollToBottom(false);
  37. }, [viewingConversation?.id]);
  38. const sendNewMessage = async () => {
  39. if (inputMsg.trim().length === 0 || isGenerating(currConvId)) return;
  40. const convId = viewingConversation?.id ?? StorageUtils.getNewConvId();
  41. const lastInpMsg = inputMsg;
  42. setInputMsg('');
  43. if (!viewingConversation) {
  44. // if user is creating a new conversation, redirect to the new conversation
  45. navigate(`/chat/${convId}`);
  46. }
  47. scrollToBottom(false);
  48. // auto scroll as message is being generated
  49. const onChunk = () => scrollToBottom(true);
  50. if (!(await sendMessage(convId, inputMsg, onChunk))) {
  51. // restore the input message if failed
  52. setInputMsg(lastInpMsg);
  53. }
  54. };
  55. return (
  56. <>
  57. {/* chat messages */}
  58. <div
  59. id="messages-list"
  60. className="flex flex-col grow overflow-y-auto"
  61. ref={containerRef}
  62. >
  63. <div className="mt-auto flex justify-center">
  64. {/* placeholder to shift the message to the bottom */}
  65. {viewingConversation ? '' : 'Send a message to start'}
  66. </div>
  67. {viewingConversation?.messages.map((msg) => (
  68. <ChatMessage key={msg.id} msg={msg} scrollToBottom={scrollToBottom} />
  69. ))}
  70. {pendingMsg && (
  71. <ChatMessage
  72. msg={pendingMsg}
  73. scrollToBottom={scrollToBottom}
  74. isPending
  75. id="pending-msg"
  76. />
  77. )}
  78. </div>
  79. {/* chat input */}
  80. <div className="flex flex-row items-center mt-8 mb-6">
  81. <textarea
  82. className="textarea textarea-bordered w-full"
  83. placeholder="Type a message (Shift+Enter to add a new line)"
  84. value={inputMsg}
  85. onChange={(e) => setInputMsg(e.target.value)}
  86. onKeyDown={(e) => {
  87. if (e.key === 'Enter' && e.shiftKey) return;
  88. if (e.key === 'Enter' && !e.shiftKey) {
  89. e.preventDefault();
  90. sendNewMessage();
  91. }
  92. }}
  93. id="msg-input"
  94. dir="auto"
  95. ></textarea>
  96. {isGenerating(currConvId) ? (
  97. <button
  98. className="btn btn-neutral ml-2"
  99. onClick={() => stopGenerating(currConvId)}
  100. >
  101. Stop
  102. </button>
  103. ) : (
  104. <button
  105. className="btn btn-primary ml-2"
  106. onClick={sendNewMessage}
  107. disabled={inputMsg.trim().length === 0}
  108. >
  109. Send
  110. </button>
  111. )}
  112. </div>
  113. </>
  114. );
  115. }