From 27ecedb02ef79ce53bf533e016b00462c44541be Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Tue, 23 May 2023 23:45:12 -0400 Subject: copying from old repo --- extension/react-app/src/tabs/additionalContext.tsx | 18 ++ extension/react-app/src/tabs/chat/MessageDiv.tsx | 73 ++++++ extension/react-app/src/tabs/chat/index.tsx | 267 +++++++++++++++++++ extension/react-app/src/tabs/main.tsx | 189 ++++++++++++++ extension/react-app/src/tabs/notebook.tsx | 285 +++++++++++++++++++++ extension/react-app/src/tabs/welcome.tsx | 22 ++ 6 files changed, 854 insertions(+) create mode 100644 extension/react-app/src/tabs/additionalContext.tsx create mode 100644 extension/react-app/src/tabs/chat/MessageDiv.tsx create mode 100644 extension/react-app/src/tabs/chat/index.tsx create mode 100644 extension/react-app/src/tabs/main.tsx create mode 100644 extension/react-app/src/tabs/notebook.tsx create mode 100644 extension/react-app/src/tabs/welcome.tsx (limited to 'extension/react-app/src/tabs') diff --git a/extension/react-app/src/tabs/additionalContext.tsx b/extension/react-app/src/tabs/additionalContext.tsx new file mode 100644 index 00000000..98fce9f1 --- /dev/null +++ b/extension/react-app/src/tabs/additionalContext.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { H3, TextArea } from "../components"; + +function AdditionalContextTab() { + return ( +
+

Additional Context

+ +

+
+ ); +} + +export default AdditionalContextTab; 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([]); + const isStreaming = useSelector(selectIsStreaming); + + useEffect(() => { + if (!isStreaming) { + hljs.highlightAll(); + } + }, [richContent, isStreaming]); + + useEffect(() => { + setRichContent([{props.content}]); + }, [props.content]); + + return ( + <> +
+ {richContent} +
+ + ); +} + +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, 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; diff --git a/extension/react-app/src/tabs/main.tsx b/extension/react-app/src/tabs/main.tsx new file mode 100644 index 00000000..a8b3300d --- /dev/null +++ b/extension/react-app/src/tabs/main.tsx @@ -0,0 +1,189 @@ +import React, { useEffect, useState } from "react"; +import { H3, TextArea, Button, Pre, Loader } from "../components"; +import styled from "styled-components"; +import { postVscMessage, withProgress } from "../vscode"; +import { useDebugContextValue } from "../redux/hooks"; +import CodeMultiselect from "../components/CodeMultiselect"; +import { useSelector } from "react-redux"; +import { selectDebugContext } from "../redux/selectors/debugContextSelectors"; +import { useDispatch } from "react-redux"; +import { updateValue } from "../redux/slices/debugContexSlice"; +import { setWorkspacePath } from "../redux/slices/configSlice"; +import { SerializedDebugContext } from "../../../src/client"; +import { useEditCache } from "../util/editCache"; +import { useApi } from "../util/api"; + +const ButtonDiv = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; + margin: 4px; + flex-wrap: wrap; + + & button { + flex-grow: 1; + } +`; + +function MainTab(props: any) { + const dispatch = useDispatch(); + + const [suggestion, setSuggestion] = useState(""); + const [traceback, setTraceback] = useDebugContextValue("traceback", ""); + const [selectedRanges, setSelectedRanges] = useDebugContextValue( + "rangesInFiles", + [] + ); + + const editCache = useEditCache(); + const { debugApi } = useApi(); + + const [responseLoading, setResponseLoading] = useState(false); + + let debugContext = useSelector(selectDebugContext); + + useEffect(() => { + editCache.preloadEdit(debugContext); + }, [debugContext]); + + function postVscMessageWithDebugContext( + type: string, + overrideDebugContext: SerializedDebugContext | null = null + ) { + postVscMessage(type, { + debugContext: overrideDebugContext || debugContext, + }); + } + + function launchFindSuspiciousCode(newTraceback: string) { + // setTraceback's effects don't occur immediately, so we have to add it to the debug context manually + let updatedDebugContext = { + ...debugContext, + traceback: newTraceback, + }; + postVscMessageWithDebugContext("findSuspiciousCode", updatedDebugContext); + postVscMessageWithDebugContext("preloadEdit", updatedDebugContext); + } + + useEffect(() => { + const eventListener = (event: any) => { + switch (event.data.type) { + case "suggestFix": + case "explainCode": + case "listTenThings": + setSuggestion(event.data.value); + setResponseLoading(false); + break; + case "traceback": + setTraceback(event.data.value); + launchFindSuspiciousCode(event.data.value); + break; + case "workspacePath": + dispatch(setWorkspacePath(event.data.value)); + break; + } + }; + window.addEventListener("message", eventListener); + + return () => window.removeEventListener("message", eventListener); + }, [debugContext, selectedRanges]); + + return ( +
+

Debug Panel

+ +

Code Sections

+ + +

Bug Description

+ + +

Stack Trace

+ + + + + + + + + + + + + + +

+
+ ); +} + +export default MainTab; diff --git a/extension/react-app/src/tabs/notebook.tsx b/extension/react-app/src/tabs/notebook.tsx new file mode 100644 index 00000000..a9c69c5b --- /dev/null +++ b/extension/react-app/src/tabs/notebook.tsx @@ -0,0 +1,285 @@ +import styled from "styled-components"; +import { + Button, + defaultBorderRadius, + vscBackground, + MainTextInput, + Loader, +} from "../components"; +import ContinueButton from "../components/ContinueButton"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { History } from "../../../schema/History"; +import { HistoryNode } from "../../../schema/HistoryNode"; +import StepContainer from "../components/StepContainer"; +import { useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; +import useContinueWebsocket from "../hooks/useWebsocket"; + +let TopNotebookDiv = styled.div` + display: grid; + grid-template-columns: 1fr; +`; + +let UserInputQueueItem = styled.div` + border-radius: ${defaultBorderRadius}; + color: gray; + padding: 8px; + margin: 8px; + text-align: center; +`; + +interface NotebookProps { + firstObservation?: any; +} + +function Notebook(props: NotebookProps) { + const serverUrl = useSelector((state: RootStore) => state.config.apiUrl); + + const [waitingForSteps, setWaitingForSteps] = useState(false); + const [userInputQueue, setUserInputQueue] = useState([]); + const [history, setHistory] = useState(); + // { + // timeline: [ + // { + // step: { + // name: "RunCodeStep", + // cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // description: + // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py`", + // }, + // output: [ + // { + // traceback: { + // frames: [ + // { + // filepath: + // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // lineno: 7, + // function: "", + // code: "print(sum(first, second))", + // }, + // ], + // message: "unsupported operand type(s) for +: 'int' and 'str'", + // error_type: + // ' ^^^^^^^^^^^^^^^^^^\n File "/Users/natesesti/Desktop/continue/extension/examples/python/sum.py", line 2, in sum\n return a + b\n ~~^~~\nTypeError', + // full_traceback: + // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in \n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'", + // }, + // }, + // null, + // ], + // }, + // { + // step: { + // name: "EditCodeStep", + // range_in_files: [ + // { + // filepath: + // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // range: { + // start: { + // line: 0, + // character: 0, + // }, + // end: { + // line: 6, + // character: 25, + // }, + // }, + // }, + // ], + // prompt: + // "I ran into this problem with my Python code:\n\n Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in \n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'\n\n Below are the files that might need to be fixed:\n\n {code}\n\n This is what the code should be in order to avoid the problem:\n", + // description: + // "Editing files: /Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // }, + // output: [ + // null, + // { + // reversible: true, + // actions: [ + // { + // reversible: true, + // filesystem: {}, + // filepath: + // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // range: { + // start: { + // line: 0, + // character: 0, + // }, + // end: { + // line: 6, + // character: 25, + // }, + // }, + // replacement: + // "\nfrom sum import sum\n\nfirst = 1\nsecond = 2\n\nprint(sum(first, second))", + // }, + // ], + // }, + // ], + // }, + // { + // step: { + // name: "SolveTracebackStep", + // traceback: { + // frames: [ + // { + // filepath: + // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // lineno: 7, + // function: "", + // code: "print(sum(first, second))", + // }, + // ], + // message: "unsupported operand type(s) for +: 'int' and 'str'", + // error_type: + // ' ^^^^^^^^^^^^^^^^^^\n File "/Users/natesesti/Desktop/continue/extension/examples/python/sum.py", line 2, in sum\n return a + b\n ~~^~~\nTypeError', + // full_traceback: + // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in \n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'", + // }, + // description: "Running step: SolveTracebackStep", + // }, + // output: [null, null], + // }, + // { + // step: { + // name: "RunCodeStep", + // cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py", + // description: + // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py`", + // }, + // output: [null, null], + // }, + // ], + // current_index: 0, + // } as any + // ); + + const { send: websocketSend } = useContinueWebsocket(serverUrl, (msg) => { + let data = JSON.parse(msg.data); + if (data.messageType === "state") { + setWaitingForSteps(data.state.active); + setHistory(data.state.history); + setUserInputQueue(data.state.user_input_queue); + } + }); + + // useEffect(() => { + // (async () => { + // if (sessionId && props.firstObservation) { + // let resp = await fetch(serverUrl + "/observation", { + // method: "POST", + // headers: new Headers({ + // "x-continue-session-id": sessionId, + // }), + // body: JSON.stringify({ + // observation: props.firstObservation, + // }), + // }); + // } + // })(); + // }, [props.firstObservation]); + + const mainTextInputRef = useRef(null); + + useEffect(() => { + if (mainTextInputRef.current) { + mainTextInputRef.current.focus(); + let handler = (event: any) => { + if (event.data.type === "focusContinueInput") { + mainTextInputRef.current?.focus(); + } + }; + window.addEventListener("message", handler); + return () => { + window.removeEventListener("message", handler); + }; + } + }, [mainTextInputRef]); + + const onMainTextInput = () => { + if (mainTextInputRef.current) { + let value = mainTextInputRef.current.value; + setWaitingForSteps(true); + websocketSend({ + messageType: "main_input", + value: value, + }); + setUserInputQueue((queue) => { + return [...queue, value]; + }); + mainTextInputRef.current.value = ""; + mainTextInputRef.current.style.height = ""; + } + }; + + const onStepUserInput = (input: string, index: number) => { + console.log("Sending step user input", input, index); + websocketSend({ + messageType: "step_user_input", + value: input, + index, + }); + }; + + // const iterations = useSelector(selectIterations); + return ( + + {history?.timeline.map((node: HistoryNode, index: number) => { + return ( + { + onStepUserInput(input, index); + }} + inFuture={index > history?.current_index} + historyNode={node} + onRefinement={(input: string) => { + websocketSend({ + messageType: "refinement_input", + value: input, + index, + }); + }} + onReverse={() => { + websocketSend({ + messageType: "reverse", + index, + }); + }} + /> + ); + })} + {waitingForSteps && } + +
+ {userInputQueue.map((input) => { + return {input}; + })} +
+ + { + if (e.key === "Enter") { + onMainTextInput(); + e.stopPropagation(); + e.preventDefault(); + } + }} + rows={1} + onChange={() => { + let textarea = mainTextInputRef.current!; + textarea.style.height = ""; /* Reset the height*/ + textarea.style.height = + Math.min(textarea.scrollHeight - 15, 500) + "px"; + }} + > + +
+ ); +} + +export default Notebook; diff --git a/extension/react-app/src/tabs/welcome.tsx b/extension/react-app/src/tabs/welcome.tsx new file mode 100644 index 00000000..c29d260a --- /dev/null +++ b/extension/react-app/src/tabs/welcome.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +function WelcomeTab() { + return ( +
+

Welcome to Continue

+ +

+ Learn more in the{" "} + + Continue User Guide + {" "} +

+

Send Nate or Ty your feedback:

+

1. What excites you about Continue?

+

2. What did you struggle with when using Continue?

+

3. How do you wish Continue worked?

+
+ ); +} + +export default WelcomeTab; -- cgit v1.2.3-70-g09d2