logancyang_obsidian-copilot/src/components/Chat.tsx
2023-04-08 23:28:34 -07:00

190 lines
6.1 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import SharedState, { ChatMessage, useSharedState } from '@/sharedState';
import { USER_SENDER, AI_SENDER } from '@/constants';
import {
BotIcon, RefreshIcon, SaveAsNoteIcon, UseActiveNoteAsContextIcon
} from '@/components/Icons';
import { OpenAIStream, Role } from '@/openAiStream';
import ChatMessageComponent from '@/components/ChatMessageComponent';
import ReactMarkdown from '@/components/Markdown/MemoizedReactMarkdown';
import { getChatContext } from '@/utils';
interface ChatProps {
sharedState: SharedState;
apiKey: string;
model: string;
}
const Chat: React.FC<ChatProps> = ({ sharedState, apiKey, model }) => {
const [chatHistory, addMessage] = useSharedState(sharedState);
const [inputMessage, setInputMessage] = useState('');
const [currentAiMessage, setCurrentAiMessage] = useState('');
const [currentModel, setCurrentModel] = useState(model);
const [rows, setRows] = useState(1);
const handleModelChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setCurrentModel(event.target.value);
};
const updateRows = (text: string) => {
const lineHeight = 20; // Adjust this value based on CSS line-height
const maxHeight = 200; // Match this to the max-height value in CSS
const minRows = 1;
const rowsNeeded = Math.min(
Math.max(text.split('\n').length, minRows), Math.floor(maxHeight / lineHeight)
);
setRows(rowsNeeded);
};
const scrollToBottom = () => {
const chatMessagesContainer = document.querySelector('.chat-messages');
if (chatMessagesContainer) {
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
}
};
useEffect(() => {
scrollToBottom();
}, [chatHistory]);
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputMessage(event.target.value);
updateRows(event.target.value);
};
const handleSendMessage = async () => {
if (!inputMessage) return;
const userMessage: ChatMessage = {
message: inputMessage,
sender: USER_SENDER,
};
// Add user message to chat history
addMessage(userMessage);
// Clear input
setInputMessage('');
// The number of past messages to use as context for the AI
// Use a even number. Increase this number later as needed
const chatContext = getChatContext(chatHistory, 4);
// Use OpenAIStream to send message to AI and get a response
try {
const stream = await OpenAIStream(
currentModel,
apiKey,
[
...chatContext.map((chatMessage) => {
return {
role: chatMessage.sender === USER_SENDER
? 'user' as Role : 'assistant' as Role,
content: chatMessage.message,
};
}),
{ role: 'user', content: userMessage.message },
],
);
const reader = stream.getReader();
const decoder = new TextDecoder();
let aiResponse = '';
reader.read().then(
async function processStream({ done, value }): Promise<void> {
if (done) {
// Add the full AI response to the chat history
const botMessage: ChatMessage = {
message: aiResponse,
sender: AI_SENDER,
};
addMessage(botMessage);
setCurrentAiMessage('');
return;
}
// Accumulate the AI response
aiResponse += decoder.decode(value);
setCurrentAiMessage(aiResponse);
// Continue reading the stream
return reader.read().then(processStream);
},
);
} catch (error) {
console.error('Error in OpenAIStream:', error);
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevents adding a newline to the textarea
handleSendMessage();
}
};
return (
<div className="chat-container">
<div className="chat-messages">
{chatHistory.map((message, index) => (
<ChatMessageComponent key={index} message={message} />
))}
{currentAiMessage && (
<div className="message bot-message">
<div className="message-icon">
<BotIcon />
</div>
<div className="message-content">
<ReactMarkdown>{currentAiMessage}</ReactMarkdown>
</div>
</div>
)}
</div>
<div className='bottom-container'>
<div className='chat-icons-container'>
<div className="chat-icon-selection-tooltip">
<div className="select-wrapper">
<select
id="aiModelSelect"
className='chat-icon-selection'
value={currentModel}
onChange={handleModelChange}
>
<option value='gpt-3.5-turbo'>GPT-3.5</option>
<option value='gpt-4'>GPT-4</option>
</select>
<span className="tooltip-text">Model Selection</span>
</div>
</div>
<button className='chat-icon-button'>
<RefreshIcon className='icon-scaler' />
<span className="tooltip-text">New Chat<br/>(unsaved history will be lost)</span>
</button>
<button className='chat-icon-button'>
<SaveAsNoteIcon className='icon-scaler' />
<span className="tooltip-text">Save as Note</span>
</button>
<button className='chat-icon-button'>
<UseActiveNoteAsContextIcon className='icon-scaler' />
<span className="tooltip-text">Use Active Note as Context</span>
</button>
</div>
<div className="chat-input-container">
<textarea
className="chat-input-textarea"
placeholder="Enter your message here..."
value={inputMessage}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
rows={rows}
/>
<button onClick={handleSendMessage}>Send</button>
</div>
</div>
</div>
);
};
export default Chat;