import React, { useCallback, useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { selectChatMessages } from "../../redux/selectors/chatSelectors"; import MessageDiv from "./MessageDiv"; import styled from "styled-components"; import { addMessage, setIsStreaming } from "../../redux/slices/chatSlice"; import { AnyAction, Dispatch } from "@reduxjs/toolkit"; import { closeStream, streamUpdate } from "../../redux/slices/chatSlice"; import { ChatMessage, RootStore } from "../../redux/store"; import { postVscMessage, vscRequest } from "../../vscode"; import { defaultBorderRadius, Loader } from "../../components"; import { selectHighlightedCode } from "../../redux/selectors/miscSelectors"; import { readRangeInVirtualFileSystem } from "../../util"; import { selectDebugContext } from "../../redux/selectors/debugContextSelectors"; let textEntryBarHeight = "30px"; const ChatContainer = styled.div` display: grid; grid-template-rows: 1fr auto; height: 100%; `; const BottomDiv = styled.div` display: grid; grid-template-rows: auto auto; `; const BottomButton = styled.button( (props: { active: boolean }) => ` font-size: 10px; border: none; color: white; margin-right: 4px; cursor: pointer; background-color: ${props.active ? "black" : "gray"}; border-radius: ${defaultBorderRadius}; padding: 8px; ` ); const TextEntryBar = styled.input` height: ${textEntryBarHeight}; border-bottom-left-radius: ${defaultBorderRadius}; border-bottom-right-radius: ${defaultBorderRadius}; padding: 8px; border: 1px solid white; background-color: black; color: white; outline: none; `; function ChatTab() { const dispatch = useDispatch(); const chatMessages = useSelector(selectChatMessages); const isStreaming = useSelector((state: RootStore) => state.chat.isStreaming); const baseUrl = useSelector((state: RootStore) => state.config.apiUrl); const debugContext = useSelector(selectDebugContext); const [includeHighlightedCode, setIncludeHighlightedCode] = useState(true); const [writeToEditor, setWriteToEditor] = useState(false); const [waitingForResponse, setWaitingForResponse] = useState(false); const highlightedCode = useSelector(selectHighlightedCode); const streamToStateThunk = useCallback( (dispatch: Dispatch, getResponse: () => Promise) => { let streamToCursor = writeToEditor; getResponse().then((resp) => { setWaitingForResponse(false); if (resp.body) { resp.body.pipeTo( new WritableStream({ write(chunk) { let update = new TextDecoder("utf-8").decode(chunk); dispatch(streamUpdate(update)); if (streamToCursor) { postVscMessage("streamUpdate", { update }); } }, close() { dispatch(closeStream()); if (streamToCursor) { postVscMessage("closeStream", null); } }, }) ); } }); }, [writeToEditor] ); const compileHiddenChatMessages = useCallback(async () => { let messages: ChatMessage[] = []; if ( includeHighlightedCode && highlightedCode?.filepath !== undefined && highlightedCode?.range !== undefined && debugContext.filesystem[highlightedCode.filepath] !== undefined ) { let fileContents = readRangeInVirtualFileSystem( highlightedCode, debugContext.filesystem ); if (fileContents) { messages.push({ role: "user", content: fileContents, }); } } else { // Similarity search over workspace let data = await vscRequest("queryEmbeddings", { query: chatMessages[chatMessages.length - 1].content, }); let codeContextMessages = data.results.map( (result: { id: string; document: string }) => { let msg: ChatMessage = { role: "user", content: `File: ${result.id} \n ${result.document}`, }; return msg; } ); codeContextMessages.push({ role: "user", content: "Use the above code to help you answer the question below. Answer in asterisk bullet points, and give the full path whenever you reference files.", }); messages.push(...codeContextMessages); } let systemMsgContent = writeToEditor ? "Respond only with the exact code requested, no additional text." : "Use the above code to help you answer the question below. Respond in markdown if using bullets or other special formatting, being sure to specify language for code blocks."; messages.push({ role: "system", content: systemMsgContent, }); return messages; }, [highlightedCode, chatMessages, includeHighlightedCode, writeToEditor]); useEffect(() => { if ( chatMessages.length > 0 && chatMessages[chatMessages.length - 1].role === "user" && !isStreaming ) { dispatch(setIsStreaming(true)); streamToStateThunk(dispatch, async () => { if (chatMessages.length === 0) { return new Promise((resolve, _) => resolve(new Response())); } let hiddenChatMessages = await compileHiddenChatMessages(); let augmentedMessages = [ ...chatMessages.slice(0, -1), ...hiddenChatMessages, chatMessages[chatMessages.length - 1], ]; console.log(augmentedMessages); // The autogenerated client can't handle streams, so have to go raw return fetch(`${baseUrl}/chat/complete`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messages: augmentedMessages, }), }); }); } }, [chatMessages, dispatch, isStreaming, highlightedCode]); const chatMessagesDiv = useRef(null); useEffect(() => { // Scroll to bottom let interval = setInterval(() => { if (chatMessagesDiv.current && !waitingForResponse) { chatMessagesDiv.current.scrollTop += Math.max( 4, 0.05 * chatMessagesDiv.current.scrollHeight - chatMessagesDiv.current.clientHeight - chatMessagesDiv.current.scrollTop ); if ( chatMessagesDiv.current.scrollTop >= chatMessagesDiv.current.scrollHeight - chatMessagesDiv.current.clientHeight ) { clearInterval(interval); } } }, 10); }, [chatMessages, chatMessagesDiv, waitingForResponse]); return (

Chat


{chatMessages.length > 0 ? ( chatMessages.map((message, idx) => { return ; }) ) : (

You can ask questions about your codebase or ask for code written directly in the editor.

)} {waitingForResponse && }
{/*

Highlighted code is automatically included in your chat message.

*/} { setWriteToEditor(!writeToEditor); }} > {writeToEditor ? "Writing to editor" : "Write to editor"} { setIncludeHighlightedCode(!includeHighlightedCode); }} > {includeHighlightedCode ? "Including highlighted code" : "Automatically finding relevant code"}
{ if (e.key === "Enter" && e.currentTarget.value !== "") { console.log("Sending message", e.currentTarget.value); dispatch( addMessage({ content: e.currentTarget.value, role: "user" }) ); (e.target as any).value = ""; setWaitingForResponse(true); } }} >
); } export default ChatTab;