diff options
| author | Nate Sesti <sestinj@gmail.com> | 2023-05-23 23:45:12 -0400 | 
|---|---|---|
| committer | Nate Sesti <sestinj@gmail.com> | 2023-05-23 23:45:12 -0400 | 
| commit | 27ecedb02ef79ce53bf533e016b00462c44541be (patch) | |
| tree | 402305113b6f04c3e3b3563b68d32de5ff1c69c8 /extension/react-app/src/tabs | |
| download | sncontinue-27ecedb02ef79ce53bf533e016b00462c44541be.tar.gz sncontinue-27ecedb02ef79ce53bf533e016b00462c44541be.tar.bz2 sncontinue-27ecedb02ef79ce53bf533e016b00462c44541be.zip  | |
copying from old repo
Diffstat (limited to 'extension/react-app/src/tabs')
| -rw-r--r-- | extension/react-app/src/tabs/additionalContext.tsx | 18 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/chat/MessageDiv.tsx | 73 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/chat/index.tsx | 267 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/main.tsx | 189 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/notebook.tsx | 285 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/welcome.tsx | 22 | 
6 files changed, 854 insertions, 0 deletions
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 ( +    <div className="mx-5"> +      <H3>Additional Context</H3> +      <TextArea +        rows={8} +        placeholder="Copy and paste information related to the bug from GitHub Issues, Slack threads, or other notes here." +        className="additionalContextTextarea" +      ></TextArea> +      <br></br> +    </div> +  ); +} + +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<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; 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 ( +    <div className="mx-5"> +      <h1>Debug Panel</h1> + +      <H3>Code Sections</H3> +      <CodeMultiselect></CodeMultiselect> + +      <H3>Bug Description</H3> +      <TextArea +        id="bugDescription" +        name="bugDescription" +        className="bugDescription" +        rows={4} +        cols={50} +        placeholder="Describe your bug..." +      ></TextArea> + +      <H3>Stack Trace</H3> +      <TextArea +        id="traceback" +        className="traceback" +        name="traceback" +        rows={4} +        cols={50} +        placeholder="Paste stack trace here" +        onChange={(e) => { +          setTraceback(e.target.value); +          dispatch(updateValue({ key: "traceback", value: e.target.value })); +          // postVscMessageWithDebugContext("findSuspiciousCode"); +        }} +        onPaste={(e) => { +          let pasted = e.clipboardData.getData("text"); +          console.log("PASTED", pasted); +          setTraceback(pasted); +          launchFindSuspiciousCode(pasted); +        }} +        value={traceback} +      ></TextArea> + +      <select +        hidden +        id="relevantVars" +        className="relevantVars" +        name="relevantVars" +      ></select> + +      <ButtonDiv> +        <Button +          onClick={() => { +            postVscMessageWithDebugContext("explainCode"); +            setResponseLoading(true); +          }} +        > +          Explain Code +        </Button> +        <Button +          onClick={() => { +            postVscMessageWithDebugContext("suggestFix"); +            setResponseLoading(true); +          }} +        > +          Generate Ideas +        </Button> +        <Button +          disabled={selectedRanges.length === 0} +          onClick={async () => { +            withProgress("Generating Fix", async () => { +              let edits = await editCache.getEdit(debugContext); +              postVscMessage("makeEdit", { edits }); +            }); +          }} +        > +          Suggest Fix +        </Button> +        <Button +          disabled={selectedRanges.length === 0} +          onClick={() => { +            postVscMessageWithDebugContext("generateUnitTest"); +          }} +        > +          Create Test +        </Button> +      </ButtonDiv> +      <Loader hidden={!responseLoading}></Loader> + +      <Pre +        className="fixSuggestion" +        hidden={!(typeof suggestion === "string" && suggestion.length > 0)} +      > +        {suggestion} +      </Pre> + +      <br></br> +    </div> +  ); +} + +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<string[]>([]); +  const [history, setHistory] = useState<History | undefined>(); +  //   { +  //   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: "<module>", +  //                 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 <module>\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 <module>\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: "<module>", +  //               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 <module>\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<HTMLTextAreaElement>(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 ( +    <TopNotebookDiv> +      {history?.timeline.map((node: HistoryNode, index: number) => { +        return ( +          <StepContainer +            key={index} +            onUserInput={(input: string) => { +              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 && <Loader></Loader>} + +      <div> +        {userInputQueue.map((input) => { +          return <UserInputQueueItem>{input}</UserInputQueueItem>; +        })} +      </div> + +      <MainTextInput +        ref={mainTextInputRef} +        onKeyDown={(e) => { +          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"; +        }} +      ></MainTextInput> +      <ContinueButton onClick={onMainTextInput}></ContinueButton> +    </TopNotebookDiv> +  ); +} + +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 ( +    <div className="mx-5"> +      <h1>Welcome to Continue</h1> + +      <p> +        Learn more in the{" "} +        <a href="https://www.notion.so/continue-dev/Continue-User-Guide-1c6ad99887d0474d9e42206f6c98efa4"> +          Continue User Guide +        </a>{" "} +      </p> +      <p>Send Nate or Ty your feedback:</p> +      <p>1. What excites you about Continue?</p> +      <p>2. What did you struggle with when using Continue?</p> +      <p>3. How do you wish Continue worked?</p> +    </div> +  ); +} + +export default WelcomeTab;  | 
