2025-02-08 21:54:50 +01:00
|
|
|
import { useEffect, useState } from 'react';
|
2025-02-06 17:32:29 +01:00
|
|
|
import { useAppContext } from '../utils/app.context';
|
|
|
|
|
import StorageUtils from '../utils/storage';
|
|
|
|
|
import { useNavigate } from 'react-router';
|
|
|
|
|
import ChatMessage from './ChatMessage';
|
2025-02-08 21:54:50 +01:00
|
|
|
import { CanvasType, PendingMessage } from '../utils/types';
|
|
|
|
|
import { classNames } from '../utils/misc';
|
|
|
|
|
import CanvasPyInterpreter from './CanvasPyInterpreter';
|
2025-02-06 17:32:29 +01:00
|
|
|
|
|
|
|
|
export default function ChatScreen() {
|
|
|
|
|
const {
|
|
|
|
|
viewingConversation,
|
|
|
|
|
sendMessage,
|
|
|
|
|
isGenerating,
|
|
|
|
|
stopGenerating,
|
|
|
|
|
pendingMessages,
|
2025-02-08 21:54:50 +01:00
|
|
|
canvasData,
|
2025-02-06 17:32:29 +01:00
|
|
|
} = useAppContext();
|
|
|
|
|
const [inputMsg, setInputMsg] = useState('');
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
const currConvId = viewingConversation?.id ?? '';
|
|
|
|
|
const pendingMsg: PendingMessage | undefined = pendingMessages[currConvId];
|
|
|
|
|
|
|
|
|
|
const scrollToBottom = (requiresNearBottom: boolean) => {
|
2025-02-08 21:54:50 +01:00
|
|
|
const mainScrollElem = document.getElementById('main-scroll');
|
|
|
|
|
if (!mainScrollElem) return;
|
2025-02-06 17:32:29 +01:00
|
|
|
const spaceToBottom =
|
2025-02-08 21:54:50 +01:00
|
|
|
mainScrollElem.scrollHeight -
|
|
|
|
|
mainScrollElem.scrollTop -
|
|
|
|
|
mainScrollElem.clientHeight;
|
2025-02-06 17:32:29 +01:00
|
|
|
if (!requiresNearBottom || spaceToBottom < 50) {
|
|
|
|
|
setTimeout(
|
2025-02-08 21:54:50 +01:00
|
|
|
() => mainScrollElem.scrollTo({ top: mainScrollElem.scrollHeight }),
|
2025-02-06 17:32:29 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-08 21:54:50 +01:00
|
|
|
const hasCanvas = !!canvasData;
|
|
|
|
|
|
2025-02-06 17:32:29 +01:00
|
|
|
return (
|
2025-02-08 21:54:50 +01:00
|
|
|
<div
|
|
|
|
|
className={classNames({
|
|
|
|
|
'grid lg:gap-8 grow transition-[300ms]': true,
|
|
|
|
|
'grid-cols-[1fr_0fr] lg:grid-cols-[1fr_1fr]': hasCanvas, // adapted for mobile
|
|
|
|
|
'grid-cols-[1fr_0fr]': !hasCanvas,
|
|
|
|
|
})}
|
|
|
|
|
>
|
2025-02-06 17:32:29 +01:00
|
|
|
<div
|
2025-02-08 21:54:50 +01:00
|
|
|
className={classNames({
|
|
|
|
|
'flex flex-col w-full max-w-[900px] mx-auto': true,
|
|
|
|
|
'hidden lg:flex': hasCanvas, // adapted for mobile
|
|
|
|
|
flex: !hasCanvas,
|
|
|
|
|
})}
|
2025-02-06 17:32:29 +01:00
|
|
|
>
|
2025-02-08 21:54:50 +01:00
|
|
|
{/* chat messages */}
|
|
|
|
|
<div id="messages-list" className="grow">
|
|
|
|
|
<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"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-02-06 17:32:29 +01:00
|
|
|
</div>
|
|
|
|
|
|
2025-02-08 21:54:50 +01:00
|
|
|
{/* chat input */}
|
|
|
|
|
<div className="flex flex-row items-center pt-8 pb-6 sticky bottom-0 bg-base-100">
|
|
|
|
|
<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>
|
2025-02-06 17:32:29 +01:00
|
|
|
</div>
|
2025-02-08 21:54:50 +01:00
|
|
|
<div className="w-full sticky top-[7em] h-[calc(100vh-9em)]">
|
|
|
|
|
{canvasData?.type === CanvasType.PY_INTERPRETER && (
|
|
|
|
|
<CanvasPyInterpreter />
|
2025-02-06 17:32:29 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-02-08 21:54:50 +01:00
|
|
|
</div>
|
2025-02-06 17:32:29 +01:00
|
|
|
);
|
|
|
|
|
}
|