summaryrefslogtreecommitdiff
path: root/extension/react-app/src/tabs/chat
diff options
context:
space:
mode:
authorNate Sesti <sestinj@gmail.com>2023-05-23 23:45:12 -0400
committerNate Sesti <sestinj@gmail.com>2023-05-23 23:45:12 -0400
commitf53768612b1e2268697b5444e502032ef9f3fb3c (patch)
tree4ed49b73e6bd3c2f8fceffa9643973033f87af95 /extension/react-app/src/tabs/chat
downloadsncontinue-f53768612b1e2268697b5444e502032ef9f3fb3c.tar.gz
sncontinue-f53768612b1e2268697b5444e502032ef9f3fb3c.tar.bz2
sncontinue-f53768612b1e2268697b5444e502032ef9f3fb3c.zip
copying from old repo
Diffstat (limited to 'extension/react-app/src/tabs/chat')
-rw-r--r--extension/react-app/src/tabs/chat/MessageDiv.tsx73
-rw-r--r--extension/react-app/src/tabs/chat/index.tsx267
2 files changed, 340 insertions, 0 deletions
diff --git a/extension/react-app/src/tabs/chat/MessageDiv.tsx b/extension/react-app/src/tabs/chat/MessageDiv.tsx
new file mode 100644
index 00000000..ab632220
--- /dev/null
+++ b/extension/react-app/src/tabs/chat/MessageDiv.tsx
@@ -0,0 +1,73 @@
+import React, { useEffect } from "react";
+import { ChatMessage } from "../../redux/store";
+import styled from "styled-components";
+import {
+ buttonColor,
+ defaultBorderRadius,
+ secondaryDark,
+} from "../../components";
+import VSCodeFileLink from "../../components/VSCodeFileLink";
+import ReactMarkdown from "react-markdown";
+import "../../highlight/dark.min.css";
+import hljs from "highlight.js";
+import { useSelector } from "react-redux";
+import { selectIsStreaming } from "../../redux/selectors/chatSelectors";
+
+const Container = styled.div`
+ padding-left: 8px;
+ padding-right: 8px;
+ border-radius: 8px;
+ margin: 3px;
+ width: fit-content;
+ max-width: 75%;
+ overflow: scroll;
+ word-wrap: break-word;
+ -ms-word-wrap: break-word;
+ height: fit-content;
+ overflow: hidden;
+ background-color: ${(props) => {
+ if (props.role === "user") {
+ return buttonColor;
+ } else {
+ return secondaryDark;
+ }
+ }};
+ float: ${(props) => {
+ if (props.role === "user") {
+ return "right";
+ } else {
+ return "left";
+ }
+ }};
+ display: block;
+
+ & pre {
+ border: 1px solid gray;
+ border-radius: ${defaultBorderRadius};
+ }
+`;
+
+function MessageDiv(props: ChatMessage) {
+ const [richContent, setRichContent] = React.useState<JSX.Element[]>([]);
+ const isStreaming = useSelector(selectIsStreaming);
+
+ useEffect(() => {
+ if (!isStreaming) {
+ hljs.highlightAll();
+ }
+ }, [richContent, isStreaming]);
+
+ useEffect(() => {
+ setRichContent([<ReactMarkdown key={1}>{props.content}</ReactMarkdown>]);
+ }, [props.content]);
+
+ return (
+ <>
+ <div className="overflow-auto">
+ <Container role={props.role}>{richContent}</Container>
+ </div>
+ </>
+ );
+}
+
+export default MessageDiv;
diff --git a/extension/react-app/src/tabs/chat/index.tsx b/extension/react-app/src/tabs/chat/index.tsx
new file mode 100644
index 00000000..a93ad4f9
--- /dev/null
+++ b/extension/react-app/src/tabs/chat/index.tsx
@@ -0,0 +1,267 @@
+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<AnyAction>, getResponse: () => Promise<Response>) => {
+ 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<HTMLDivElement>(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 (
+ <ChatContainer>
+ <div className="mx-5 overflow-y-scroll" ref={chatMessagesDiv}>
+ <h1>Chat</h1>
+ <hr></hr>
+ <div>
+ {chatMessages.length > 0 ? (
+ chatMessages.map((message, idx) => {
+ return <MessageDiv key={idx} {...message}></MessageDiv>;
+ })
+ ) : (
+ <p className="text-gray-400 m-auto text-center">
+ You can ask questions about your codebase or ask for code written
+ directly in the editor.
+ </p>
+ )}
+ {waitingForResponse && <Loader></Loader>}
+ </div>
+ </div>
+
+ <BottomDiv>
+ <div className="h-12 bg-secondary-">
+ <div className="flex items-center p-2">
+ {/* <p className="mr-auto text-xs">
+ Highlighted code is automatically included in your chat message.
+ </p> */}
+ <BottomButton
+ className="ml-auto"
+ active={writeToEditor}
+ onClick={() => {
+ setWriteToEditor(!writeToEditor);
+ }}
+ >
+ {writeToEditor ? "Writing to editor" : "Write to editor"}
+ </BottomButton>
+
+ <BottomButton
+ active={includeHighlightedCode}
+ onClick={() => {
+ setIncludeHighlightedCode(!includeHighlightedCode);
+ }}
+ >
+ {includeHighlightedCode
+ ? "Including highlighted code"
+ : "Automatically finding relevant code"}
+ </BottomButton>
+ </div>
+ </div>
+ <TextEntryBar
+ type="text"
+ placeholder="Enter your message here"
+ onKeyDown={(e) => {
+ 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);
+ }
+ }}
+ ></TextEntryBar>
+ </BottomDiv>
+ </ChatContainer>
+ );
+}
+
+export default ChatTab;