diff options
| author | Nate Sesti <sestinj@gmail.com> | 2023-07-16 16:25:02 -0700 | 
|---|---|---|
| committer | Nate Sesti <sestinj@gmail.com> | 2023-07-16 16:25:02 -0700 | 
| commit | da7827189181328baa329d2984d0fd9c6d476be3 (patch) | |
| tree | 56308a5595e8e01ab83974688d8ac8e4945f319a /extension/react-app/src | |
| parent | d80119982e9b60ca0022533a0086eb526dc7d957 (diff) | |
| parent | eab69781a3e3b5236916d9057ce29aba2e868913 (diff) | |
| download | sncontinue-da7827189181328baa329d2984d0fd9c6d476be3.tar.gz sncontinue-da7827189181328baa329d2984d0fd9c6d476be3.tar.bz2 sncontinue-da7827189181328baa329d2984d0fd9c6d476be3.zip | |
Merge branch 'main' into ggml-server
Diffstat (limited to 'extension/react-app/src')
23 files changed, 391 insertions, 1314 deletions
| diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index 8785f88f..c9bd42e0 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -1,8 +1,5 @@  import DebugPanel from "./components/DebugPanel"; -import MainTab from "./tabs/main"; -import WelcomeTab from "./tabs/welcome"; -import ChatTab from "./tabs/chat"; -import GUI from "./tabs/gui"; +import GUI from "./pages/gui";  import { createContext } from "react";  import useContinueGUIProtocol from "./hooks/useWebsocket";  import ContinueGUIClientProtocol from "./hooks/useContinueGUIProtocol"; @@ -18,13 +15,7 @@ function App() {      <GUIClientContext.Provider value={client}>        <DebugPanel          tabs={[ -          { -            element: <GUI />, -            title: "GUI", -          }, -          // { element: <MainTab />, title: "Debug Panel" }, -          // { element: <WelcomeTab />, title: "Welcome" }, -          // { element: <ChatTab />, title: "Chat" }, +          { element: <GUI />, title: "GUI" }          ]}        />      </GUIClientContext.Provider> diff --git a/extension/react-app/src/TestPage.tsx b/extension/react-app/src/TestPage.tsx deleted file mode 100644 index d104980b..00000000 --- a/extension/react-app/src/TestPage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import styled from "styled-components"; - -const SideBySideDiv = styled.div` -  display: grid; -  grid-template-columns: 1fr 1fr; -  grid-template-rows: 1fr; -  grid-template-areas: "left right"; -`; - -const LeftDiv = styled.div` -  grid-area: left; -`; - -const RightDiv = styled.div` -  grid-area: right; -`; - -function TestPage() { -  return ( -    <div> -      <h1>Continue</h1> -      <SideBySideDiv> -        <LeftDiv> -          <h2>Left</h2> -        </LeftDiv> -        <RightDiv> -          <h2>Right</h2> -        </RightDiv> -      </SideBySideDiv> -    </div> -  ); -} diff --git a/extension/react-app/src/assets/Hubot-Sans.woff2 b/extension/react-app/src/assets/Hubot-Sans.woff2Binary files differ deleted file mode 100644 index 5089fc47..00000000 --- a/extension/react-app/src/assets/Hubot-Sans.woff2 +++ /dev/null diff --git a/extension/react-app/src/assets/Mona-Sans.woff2 b/extension/react-app/src/assets/Mona-Sans.woff2Binary files differ deleted file mode 100644 index 8208a500..00000000 --- a/extension/react-app/src/assets/Mona-Sans.woff2 +++ /dev/null diff --git a/extension/react-app/src/assets/react.svg b/extension/react-app/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/extension/react-app/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file diff --git a/extension/react-app/src/components/CodeMultiselect.tsx b/extension/react-app/src/components/CodeMultiselect.tsx deleted file mode 100644 index c0ab9400..00000000 --- a/extension/react-app/src/components/CodeMultiselect.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styled from "styled-components"; -import { Button, buttonColor, defaultBorderRadius, secondaryDark } from "."; -import { useSelector } from "react-redux"; -import { -  selectDebugContext, -  selectAllRangesInFiles, -  selectRangesMask, -} from "../redux/selectors/debugContextSelectors"; -import "../highlight/dark.min.css"; -import hljs from "highlight.js"; -import { postVscMessage } from "../vscode"; -import { RootStore } from "../redux/store"; -import { useDispatch } from "react-redux"; -import { -  addRangeInFile, -  deleteRangeInFileAt, -  toggleSelectionAt, -  updateFileSystem, -} from "../redux/slices/debugContexSlice"; -import { RangeInFile } from "../../../src/client"; -import { readRangeInVirtualFileSystem } from "../util"; - -//#region Styled Components - -const MultiSelectContainer = styled.div` -  border-radius: ${defaultBorderRadius}; -  padding: 4px; -  display: flex; -  flex-direction: column; -  gap: 4px; -`; - -const MultiSelectHeader = styled.div` -  display: flex; -  justify-content: space-between; -  align-items: left; -  border-bottom: 1px solid gray; -  padding-left: 4px; -  padding-right: 4px; -  & p { -    overflow-wrap: break-word; -    word-wrap: break-word; -    -ms-wrap-flow: break-word; -    overflow: hidden; -  } -`; - -const MultiSelectOption = styled.div` -  border-radius: ${defaultBorderRadius}; -  padding-top: 4px; -  cursor: pointer; -  background-color: ${secondaryDark}; -`; - -const DeleteSelectedRangeButton = styled(Button)` -  align-self: right; -  padding: 0px; -  margin-top: 0; -  aspect-ratio: 1/1; -  height: 28px; -`; - -const ToggleHighlightButton = styled(Button)` -  display: grid; -  justify-content: center; -  align-items: center; -  grid-template-columns: 30px 1fr; -  margin-left: 20px; -  order: 1; -  width: fit-content; -`; - -//#endregion - -//#region Path Formatting - -const filenameToLanguageMap: any = { -  py: "python", -  js: "javascript", -  ts: "typescript", -  html: "html", -  css: "css", -  java: "java", -  c: "c", -  cpp: "cpp", -  cs: "csharp", -  go: "go", -  rb: "ruby", -  rs: "rust", -  swift: "swift", -  php: "php", -  scala: "scala", -  kt: "kotlin", -  dart: "dart", -  hs: "haskell", -  lua: "lua", -  pl: "perl", -  r: "r", -  sql: "sql", -  vb: "vb", -  xml: "xml", -  yaml: "yaml", -}; - -function filenameToLanguage(filename: string): string { -  const extension = filename.split(".").pop(); -  if (extension === undefined) { -    return ""; -  } -  return filenameToLanguageMap[extension] || ""; -} - -function formatPathRelativeToWorkspace( -  path: string, -  workspacePath: string | undefined -) { -  if (workspacePath === undefined) { -    return path; -  } -  if (path.startsWith(workspacePath)) { -    return path.substring(workspacePath.length + 1); -  } else { -    return path; -  } -} - -function formatFileRange( -  rangeInFile: RangeInFile, -  workspacePath: string | undefined -) { -  return `${formatPathRelativeToWorkspace( -    rangeInFile.filepath, -    workspacePath -  )} (lines ${rangeInFile.range.start.line + 1}-${ -    rangeInFile.range.end.line + 1 -  })`; -  // +1 because VS Code Ranges are 0-indexed -} - -//#endregion - -function CodeMultiselect(props: {}) { -  // State -  const [highlightLocked, setHighlightLocked] = useState(true); - -  // Redux -  const dispatch = useDispatch(); -  const workspacePath = useSelector( -    (state: RootStore) => state.config.workspacePath -  ); -  const debugContext = useSelector(selectDebugContext); -  const rangesInFiles = useSelector(selectAllRangesInFiles); -  const rangesInFilesMask = useSelector(selectRangesMask); - -  useEffect(() => { -    let eventListener = (event: any) => { -      switch (event.data.type) { -        case "highlightedCode": -          if (!highlightLocked) { -            dispatch( -              addRangeInFile({ -                rangeInFile: event.data.rangeInFile, -                canUpdateLast: true, -              }) -            ); -            dispatch(updateFileSystem(event.data.filesystem)); -          } -          break; -        case "findSuspiciousCode": -          for (let c of event.data.codeLocations) { -            dispatch(addRangeInFile({ rangeInFile: c, canUpdateLast: false })); -          } -          dispatch(updateFileSystem(event.data.filesystem)); -          postVscMessage("listTenThings", { debugContext }); -          break; -      } -    }; -    window.addEventListener("message", eventListener); -    return () => window.removeEventListener("message", eventListener); -  }, [debugContext, highlightLocked]); - -  useEffect(() => { -    hljs.highlightAll(); -  }, [rangesInFiles]); - -  return ( -    <MultiSelectContainer> -      {rangesInFiles.map((range: RangeInFile, index: number) => { -        return ( -          <MultiSelectOption -            key={index} -            style={{ -              border: `1px solid ${ -                rangesInFilesMask[index] ? buttonColor : "gray" -              }`, -            }} -            onClick={() => { -              dispatch(toggleSelectionAt(index)); -            }} -          > -            <MultiSelectHeader> -              <p style={{ margin: "4px" }}> -                {formatFileRange(range, workspacePath)} -              </p> -              <DeleteSelectedRangeButton -                onClick={() => dispatch(deleteRangeInFileAt(index))} -              > -                x -              </DeleteSelectedRangeButton> -            </MultiSelectHeader> -            <pre> -              <code -                className={"language-" + filenameToLanguage(range.filepath)} -              > -                {readRangeInVirtualFileSystem(range, debugContext.filesystem)} -              </code> -            </pre> -          </MultiSelectOption> -        ); -      })} -      {rangesInFiles.length === 0 && ( -        <> -          <p>Highlight relevant code in the editor.</p> -        </> -      )} -      <ToggleHighlightButton -        onClick={() => { -          setHighlightLocked(!highlightLocked); -        }} -      > -        {highlightLocked ? ( -          <> -            <svg -              xmlns="http://www.w3.org/2000/svg" -              width="20px" -              fill="none" -              viewBox="0 0 24 24" -              strokeWidth="1.5" -              stroke="currentColor" -              className="w-6 h-6" -            > -              <path -                strokeLinecap="round" -                strokeLinejoin="round" -                d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" -              /> -            </svg>{" "} -            Enable Highlight -          </> -        ) : ( -          <> -            <svg -              xmlns="http://www.w3.org/2000/svg" -              width="20px" -              fill="none" -              viewBox="0 0 24 24" -              strokeWidth="1.5" -              stroke="currentColor" -              className="w-6 h-6" -            > -              <path -                strokeLinecap="round" -                strokeLinejoin="round" -                d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" -              /> -            </svg>{" "} -            Disable Highlight -          </> -        )} -      </ToggleHighlightButton> -    </MultiSelectContainer> -  ); -} - -export default CodeMultiselect; diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 801c3a03..f11e07af 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -1,30 +1,19 @@ -import React, { -  useCallback, -  useEffect, -  useImperativeHandle, -  useState, -} from "react"; +import React, { useEffect, useImperativeHandle, useState } from "react";  import { useCombobox } from "downshift";  import styled from "styled-components";  import { -  buttonColor,    defaultBorderRadius,    lightGray,    secondaryDark,    vscBackground,  } from ".";  import CodeBlock from "./CodeBlock"; -import { RangeInFile } from "../../../src/client";  import PillButton from "./PillButton";  import HeaderButtonWithText from "./HeaderButtonWithText"; -import { -  Trash, -  LockClosed, -  LockOpen, -  Plus, -  DocumentPlus, -} from "@styled-icons/heroicons-outline"; +import { DocumentPlus } from "@styled-icons/heroicons-outline";  import { HighlightedRangeContext } from "../../../schema/FullState"; +import { postVscMessage } from "../vscode"; +import { getMetaKeyLabel } from "../util";  // #region styled components  const mainInputFontSize = 13; @@ -180,6 +169,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {    useImperativeHandle(ref, () => downshiftProps, [downshiftProps]); +  const [metaKeyPressed, setMetaKeyPressed] = useState(false); +  const [focused, setFocused] = useState(false); +  useEffect(() => { +    const handleKeyDown = (e: KeyboardEvent) => { +      if (e.key === "Meta") { +        setMetaKeyPressed(true); +      } +    }; +    const handleKeyUp = (e: KeyboardEvent) => { +      if (e.key === "Meta") { +        setMetaKeyPressed(false); +      } +    }; +    window.addEventListener("keydown", handleKeyDown); +    window.addEventListener("keyup", handleKeyUp); +    return () => { +      window.removeEventListener("keydown", handleKeyDown); +      window.removeEventListener("keyup", handleKeyUp); +    }; +  }); +    useEffect(() => {      if (!inputRef.current) {        return; @@ -221,6 +231,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {          )} */}          {highlightedCodeSections.map((section, idx) => (            <PillButton +            warning={ +              section.range.contents.length > 4000 && section.editing +                ? "Editing such a large range may be slow" +                : undefined +            }              editing={section.editing}              pinned={section.pinned}              index={idx} @@ -272,7 +287,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {        <div className="flex px-2" ref={divRef} hidden={!downshiftProps.isOpen}>          <MainTextInput            disabled={props.disabled} -          placeholder="Ask a question, give instructions, or type '/' to see slash commands" +          placeholder={`Ask a question, give instructions, or type '/' to see slash commands. ${getMetaKeyLabel()}⏎ to edit.`}            {...getInputProps({              onChange: (e) => {                const target = e.target as HTMLTextAreaElement; @@ -285,6 +300,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                // setShowContextDropdown(target.value.endsWith("@"));              }, +            onFocus: (e) => { +              setFocused(true); +            }, +            onBlur: (e) => { +              setFocused(false); +              postVscMessage("blurContinueInput", {}); +            },              onKeyDown: (event) => {                if (event.key === "Enter" && event.shiftKey) {                  // Prevent Downshift's default 'Enter' behavior. @@ -311,7 +333,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                ) {                  (event.nativeEvent as any).preventDownshiftDefault = true;                } else if (event.key === "ArrowUp") { -                console.log("OWJFOIJO");                  if (positionInHistory == 0) return;                  else if (                    positionInHistory == history.length && @@ -357,10 +378,15 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {              ))}          </Ul>        </div> -      {/* <span className="text-trueGray-400 ml-auto m-auto text-xs text-right"> -        Highlight code to include as context. Currently open file included by -        default. {highlightedCodeSections.length === 0 && ""} -      </span> */} +      {highlightedCodeSections.length === 0 && +        (downshiftProps.inputValue?.startsWith("/edit") || +          (focused && +            metaKeyPressed && +            downshiftProps.inputValue?.length > 0)) && ( +          <div className="text-trueGray-400 pr-4 text-xs text-right"> +            Inserting at cursor +          </div> +        )}        <ContextDropdown          onMouseEnter={() => {            setHoveringContextDropdown(true); diff --git a/extension/react-app/src/components/LoadingCover.tsx b/extension/react-app/src/components/LoadingCover.tsx deleted file mode 100644 index a0f8f7a2..00000000 --- a/extension/react-app/src/components/LoadingCover.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import styled from "styled-components"; - -const StyledDiv = styled.div` -  position: absolute; -  top: 0; -  left: 0; -  width: 100%; -  height: 100vh; -  background: linear-gradient( -    101.79deg, -    #12887a 0%, -    #87245c 32%, -    #e12637 63%, -    #ffb215 100% -  ); -  display: flex; -  justify-content: center; -  align-items: center; -  flex-direction: column; -  z-index: 10; -`; - -const StyledImg = styled.img` -  /* add your styles here */ -`; - -const StyledDiv2 = styled.div` -  width: 50%; -  height: 5px; -  background: white; -  margin-top: 20px; -`; - -interface LoadingCoverProps { -  message: string; -  hidden?: boolean; -} - -const LoadingCover = (props: LoadingCoverProps) => { -  return ( -    <StyledDiv style={{ display: props.hidden ? "none" : "inherit" }}> -      <StyledImg src="continue.gif" alt="centered image" width="50%" /> -      <StyledDiv2></StyledDiv2> -      <p>{props.message}</p> -    </StyledDiv> -  ); -}; - -export default LoadingCover; diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx new file mode 100644 index 00000000..231c1e93 --- /dev/null +++ b/extension/react-app/src/components/Onboarding.tsx @@ -0,0 +1,136 @@ +import { useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; +import React, { useState, useEffect } from "react"; +import styled from "styled-components"; +import { ArrowLeft, ArrowRight } from "@styled-icons/heroicons-outline"; +import { defaultBorderRadius } from "."; +import Loader from "./Loader"; + +const StyledDiv = styled.div` +  position: absolute; +  top: 0; +  left: 0; +  width: 100%; +  height: 100%; +  background-color: #1e1e1e; +  z-index: 200; +`; + +const StyledSpan = styled.span` +  padding: 8px; +  border-radius: ${defaultBorderRadius}; +  &:hover { +    background-color: #ffffff33; +  } +  white-space: nowrap; +`; + +const Onboarding = () => { +  const [counter, setCounter] = useState(4); +  const gifs = ["intro", "highlight", "question", "help"]; +  const topMessages = [ +    "Welcome!", +    "Highlight code", +    "Ask a question", +    "Use /help to learn more", +  ]; + +  useEffect(() => { +    const hasVisited = localStorage.getItem("hasVisited"); +    if (hasVisited) { +      setCounter(4); +    } else { +      setCounter(0); +      localStorage.setItem("hasVisited", "true"); +    } +  }, []); + +  const [loading, setLoading] = useState(true); + +  useEffect(() => { +    setLoading(true); +  }, [counter]); + +  return ( +    <StyledDiv hidden={counter >= 4}> +      <div +        style={{ +          display: "grid", +          justifyContent: "center", +          alignItems: "center", +          height: "100%", +          textAlign: "center", +          background: `linear-gradient( +            101.79deg, +            #12887a66 0%, +            #87245c66 32%, +            #e1263766 63%, +            #ffb21566 100% +          )`, +          paddingLeft: "16px", +          paddingRight: "16px", +        }} +      > +        <h1>{topMessages[counter]}</h1> +        <div style={{ display: "flex", justifyContent: "center" }}> +          {loading && ( +            <div style={{ margin: "auto", position: "absolute", zIndex: 0 }}> +              <Loader /> +            </div> +          )} +          {counter % 2 === 0 ? ( +            <img +              src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`} +              width="100%" +              key={"even-gif"} +              alt={topMessages[counter]} +              onLoad={() => { +                setLoading(false); +              }} +              style={{ zIndex: 1 }} +            /> +          ) : ( +            <img +              src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`} +              width="100%" +              key={"odd-gif"} +              alt={topMessages[counter]} +              onLoad={() => { +                setLoading(false); +              }} +              style={{ zIndex: 1 }} +            /> +          )} +        </div> +        <p +          style={{ +            paddingLeft: "50px", +            paddingRight: "50px", +            paddingBottom: "50px", +            textAlign: "center", +            cursor: "pointer", +            whiteSpace: "nowrap", +          }} +        > +          <StyledSpan +            hidden={counter === 0} +            onClick={() => setCounter((prev) => Math.max(prev - 1, 0))} +          > +            <ArrowLeft width="18px" strokeWidth="2px" /> Previous +          </StyledSpan> +          <span hidden={counter === 0}>{" | "}</span> +          <StyledSpan onClick={() => setCounter((prev) => prev + 1)}> +            {counter === 0 +              ? "Click to learn how to use Continue" +              : counter === 3 +              ? "Get Started" +              : "Next"}{" "} +            <ArrowRight width="18px" strokeWidth="2px" /> +          </StyledSpan> +        </p> +      </div> +    </StyledDiv> +  ); +}; + +export default Onboarding; diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index 31d98c0f..d9d779d1 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -1,12 +1,11 @@  import { useContext, useState } from "react";  import styled from "styled-components"; +import { StyledTooltip, defaultBorderRadius, secondaryDark } from ".";  import { -  StyledTooltip, -  defaultBorderRadius, -  lightGray, -  secondaryDark, -} from "."; -import { Trash, PaintBrush, MapPin } from "@styled-icons/heroicons-outline"; +  Trash, +  PaintBrush, +  ExclamationTriangle, +} from "@styled-icons/heroicons-outline";  import { GUIClientContext } from "../App";  const Button = styled.button` @@ -31,7 +30,6 @@ const GridDiv = styled.div`    grid-template-columns: 1fr 1fr;    align-items: center;    border-radius: ${defaultBorderRadius}; -  overflow: hidden;    background-color: ${secondaryDark};  `; @@ -48,6 +46,21 @@ const ButtonDiv = styled.div<{ backgroundColor: string }>`    }  `; +const CircleDiv = styled.div` +  position: absolute; +  top: -10px; +  right: -10px; +  width: 20px; +  height: 20px; +  border-radius: 50%; +  background-color: red; +  color: white; +  display: flex; +  align-items: center; +  justify-content: center; +  padding: 2px; +`; +  interface PillButtonProps {    onHover?: (arg0: boolean) => void;    onDelete?: () => void; @@ -55,6 +68,7 @@ interface PillButtonProps {    index: number;    editing: boolean;    pinned: boolean; +  warning?: string;  }  const PillButton = (props: PillButtonProps) => { @@ -63,75 +77,96 @@ const PillButton = (props: PillButtonProps) => {    return (      <> -      <Button -        style={{ -          position: "relative", -          borderColor: props.editing -            ? "#8800aa" -            : props.pinned -            ? "#ffff0099" -            : "transparent", -          borderWidth: "1px", -          borderStyle: "solid", -        }} -        onMouseEnter={() => { -          setIsHovered(true); -          if (props.onHover) { -            props.onHover(true); -          } -        }} -        onMouseLeave={() => { -          setIsHovered(false); -          if (props.onHover) { -            props.onHover(false); -          } -        }} -      > -        {isHovered && ( -          <GridDiv> -            <ButtonDiv -              data-tooltip-id={`edit-${props.index}`} -              backgroundColor={"#8800aa55"} -              onClick={() => { -                client?.setEditingAtIndices([props.index]); -              }} -            > -              <PaintBrush style={{ margin: "auto" }} width="1.6em"></PaintBrush> -            </ButtonDiv> +      <div style={{ position: "relative" }}> +        <Button +          style={{ +            position: "relative", +            borderColor: props.warning +              ? "red" +              : props.editing +              ? "#8800aa" +              : props.pinned +              ? "#ffff0099" +              : "transparent", +            borderWidth: "1px", +            borderStyle: "solid", +          }} +          onMouseEnter={() => { +            setIsHovered(true); +            if (props.onHover) { +              props.onHover(true); +            } +          }} +          onMouseLeave={() => { +            setIsHovered(false); +            if (props.onHover) { +              props.onHover(false); +            } +          }} +        > +          {isHovered && ( +            <GridDiv> +              <ButtonDiv +                data-tooltip-id={`edit-${props.index}`} +                backgroundColor={"#8800aa55"} +                onClick={() => { +                  client?.setEditingAtIndices([props.index]); +                }} +              > +                <PaintBrush +                  style={{ margin: "auto" }} +                  width="1.6em" +                ></PaintBrush> +              </ButtonDiv> -            {/* <ButtonDiv +              {/* <ButtonDiv              data-tooltip-id={`pin-${props.index}`}              backgroundColor={"#ffff0055"}              onClick={() => {                client?.setPinnedAtIndices([props.index]);              }} -          > +            >              <MapPin style={{ margin: "auto" }} width="1.6em"></MapPin>            </ButtonDiv> */} -            <StyledTooltip id={`pin-${props.index}`}> -              Edit this range +              <StyledTooltip id={`pin-${props.index}`}> +                Edit this range +              </StyledTooltip> +              <ButtonDiv +                data-tooltip-id={`delete-${props.index}`} +                backgroundColor={"#cc000055"} +                onClick={() => { +                  if (props.onDelete) { +                    props.onDelete(); +                  } +                }} +              > +                <Trash style={{ margin: "auto" }} width="1.6em"></Trash> +              </ButtonDiv> +            </GridDiv> +          )} +          {props.title} +        </Button> +        <StyledTooltip id={`edit-${props.index}`}> +          {props.editing +            ? "Editing this range (with rest of file as context)" +            : "Edit this range"} +        </StyledTooltip> +        <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip> +        {props.warning && ( +          <> +            <CircleDiv data-tooltip-id={`circle-div-${props.title}`}> +              <ExclamationTriangle +                style={{ margin: "auto" }} +                width="1.0em" +                strokeWidth={2} +              /> +            </CircleDiv> +            <StyledTooltip id={`circle-div-${props.title}`}> +              {props.warning}              </StyledTooltip> -            <ButtonDiv -              data-tooltip-id={`delete-${props.index}`} -              backgroundColor={"#cc000055"} -              onClick={() => { -                if (props.onDelete) { -                  props.onDelete(); -                } -              }} -            > -              <Trash style={{ margin: "auto" }} width="1.6em"></Trash> -            </ButtonDiv> -          </GridDiv> +          </>          )} -        {props.title} -      </Button> -      <StyledTooltip id={`edit-${props.index}`}> -        {props.editing -          ? "Editing this range (with rest of file as context)" -          : "Edit this range"} -      </StyledTooltip> -      <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip> +      </div>      </>    );  }; diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index d480c565..93bdbc89 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react";  import styled, { keyframes } from "styled-components";  import {    appear, @@ -15,9 +15,9 @@ import {  } from "@styled-icons/heroicons-outline";  import { StopCircle } from "@styled-icons/heroicons-solid";  import { HistoryNode } from "../../../schema/HistoryNode"; -import ReactMarkdown from "react-markdown";  import HeaderButtonWithText from "./HeaderButtonWithText"; -import CodeBlock from "./CodeBlock"; +import MarkdownPreview from "@uiw/react-markdown-preview"; +import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";  interface StepContainerProps {    historyNode: HistoryNode; @@ -72,19 +72,6 @@ const ContentDiv = styled.div<{ isUserInput: boolean }>`    font-size: 13px;  `; -const MarkdownPre = styled.pre` -  background-color: ${secondaryDark}; -  padding: 10px; -  border-radius: ${defaultBorderRadius}; -  border: 0.5px solid white; -`; - -const StyledCode = styled.code` -  word-wrap: break-word; -  color: #f69292; -  background: transparent; -`; -  const gradient = keyframes`    0% {      background-position: 0px 0; @@ -124,6 +111,31 @@ const GradientBorder = styled.div<{    background-size: 200% 200%;  `; +const StyledMarkdownPreview = styled(MarkdownPreview)` +  pre { +    background-color: ${secondaryDark}; +    padding: 1px; +    border-radius: ${defaultBorderRadius}; +    border: 0.5px solid white; +  } + +  code { +    color: #f69292; +    word-wrap: break-word; +  } + +  pre > code { +    background-color: ${secondaryDark}; +    color: white; +  } + +  background-color: ${vscBackground}; +  font-family: "Lexend", sans-serif; +  font-size: 13px; +  padding: 8px; +  color: white; +`; +  // #endregion  function StepContainer(props: StepContainerProps) { @@ -158,7 +170,7 @@ function StepContainer(props: StepContainerProps) {      >        <StepContainerDiv open={props.open}>          <GradientBorder -          loading={props.historyNode.active as boolean | false} +          loading={(props.historyNode.active as boolean) || false}            isFirst={props.isFirst}            isLast={props.isLast}            borderColor={ @@ -170,7 +182,7 @@ function StepContainer(props: StepContainerProps) {            }            className="overflow-hidden cursor-pointer"            onClick={(e) => { -            if (e.metaKey) { +            if (isMetaEquivalentKeyPressed(e)) {                props.onToggleAll();              } else {                props.onToggle(); @@ -178,7 +190,7 @@ function StepContainer(props: StepContainerProps) {            }}          >            <HeaderDiv -            loading={props.historyNode.active as boolean | false} +            loading={(props.historyNode.active as boolean) || false}              error={props.historyNode.observation?.error ? true : false}            >              <div className="m-2"> @@ -206,7 +218,11 @@ function StepContainer(props: StepContainerProps) {                    e.stopPropagation();                    props.onDelete();                  }} -                text={props.historyNode.active ? "Stop (⌘⌫)" : "Delete"} +                text={ +                  props.historyNode.active +                    ? `Stop (${getMetaKeyLabel()}⌫)` +                    : "Delete" +                }                >                  {props.historyNode.active ? (                    <StopCircle size="1.6em" onClick={props.onDelete} /> @@ -242,31 +258,16 @@ function StepContainer(props: StepContainerProps) {            )}            {props.historyNode.observation?.error ? ( -            <pre className="overflow-x-scroll"> -              {props.historyNode.observation.error as string} -            </pre> +            <details> +              <summary>View Traceback</summary> +              <pre className="overflow-x-scroll"> +                {props.historyNode.observation.error as string} +              </pre> +            </details>            ) : ( -            <ReactMarkdown -              key={1} -              className="overflow-x-scroll" -              components={{ -                pre: ({ node, ...props }) => { -                  return ( -                    <CodeBlock -                      children={(props.children[0] as any).props.children[0]} -                    /> -                  ); -                }, -                code: ({ node, ...props }) => { -                  return <StyledCode children={props.children[0] as any} />; -                }, -                ul: ({ node, ...props }) => { -                  return <ul className="ml-0" {...props} />; -                }, -              }} -            > -              {props.historyNode.step.description as any} -            </ReactMarkdown> +            <StyledMarkdownPreview +              source={props.historyNode.step.description || ""} +            />            )}          </ContentDiv>        </StepContainerDiv> diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx index ea5727f0..646d6846 100644 --- a/extension/react-app/src/components/TextDialog.tsx +++ b/extension/react-app/src/components/TextDialog.tsx @@ -2,6 +2,7 @@  import React, { useEffect, useState } from "react";  import styled from "styled-components";  import { Button, buttonColor, secondaryDark, vscBackground } from "."; +import { isMetaEquivalentKeyPressed } from "../util";  const ScreenCover = styled.div`    position: absolute; @@ -81,7 +82,11 @@ const TextDialog = (props: {              rows={10}              ref={textAreaRef}              onKeyDown={(e) => { -              if (e.key === "Enter" && e.metaKey && textAreaRef.current) { +              if ( +                e.key === "Enter" && +                isMetaEquivalentKeyPressed(e) && +                textAreaRef.current +              ) {                  props.onEnter(textAreaRef.current.value);                  setText("");                } else if (e.key === "Escape") { diff --git a/extension/react-app/src/highlight/dark.min.css b/extension/react-app/src/highlight/dark.min.css deleted file mode 100644 index 9268d7c9..00000000 --- a/extension/react-app/src/highlight/dark.min.css +++ /dev/null @@ -1,53 +0,0 @@ -pre code.hljs { -  display: block; -  overflow-x: auto; -  padding: 1em; -} -code.hljs { -  padding: 3px 5px; -} -.hljs { -  color: #ddd; -  background: #252526; -} -.hljs-keyword, -.hljs-link, -.hljs-literal, -.hljs-section, -.hljs-selector-tag { -  color: #fff; -} -.hljs-addition, -.hljs-attribute, -.hljs-built_in, -.hljs-bullet, -.hljs-name, -.hljs-string, -.hljs-symbol, -.hljs-template-tag, -.hljs-template-variable, -.hljs-title, -.hljs-type, -.hljs-variable { -  color: #d88; -} -.hljs-comment, -.hljs-deletion, -.hljs-meta, -.hljs-quote { -  color: #979797; -} -.hljs-doctag, -.hljs-keyword, -.hljs-literal, -.hljs-name, -.hljs-section, -.hljs-selector-tag, -.hljs-strong, -.hljs-title, -.hljs-type { -  font-weight: 700; -} -.hljs-emphasis { -  font-style: italic; -} diff --git a/extension/react-app/src/hooks/messenger.ts b/extension/react-app/src/hooks/messenger.ts index e2a0bab8..00ce1fbb 100644 --- a/extension/react-app/src/hooks/messenger.ts +++ b/extension/react-app/src/hooks/messenger.ts @@ -1,6 +1,3 @@ -// console.log("Websocket import"); -// const WebSocket = require("ws"); -  export abstract class Messenger {    abstract send(messageType: string, data: object): void; @@ -28,13 +25,6 @@ export class WebsocketMessenger extends Messenger {    private serverUrl: string;    _newWebsocket(): WebSocket { -    // // Dynamic import, because WebSocket is builtin with browser, but not with node. And can't use require in browser. -    // if (typeof process === "object") { -    //   console.log("Using node"); -    //   // process is only available in Node -    //   var WebSocket = require("ws"); -    // } -      const newWebsocket = new WebSocket(this.serverUrl);      for (const listener of this.onOpenListeners) {        this.onOpen(listener); diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/pages/gui.tsx index e1ecec9e..64207487 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -20,9 +20,10 @@ import ReactSwitch from "react-switch";  import { usePostHog } from "posthog-js/react";  import { useSelector } from "react-redux";  import { RootStore } from "../redux/store"; -import LoadingCover from "../components/LoadingCover";  import { postVscMessage } from "../vscode";  import UserInputContainer from "../components/UserInputContainer"; +import Onboarding from "../components/Onboarding"; +import { isMetaEquivalentKeyPressed } from "../util";  const TopGUIDiv = styled.div`    overflow: hidden; @@ -95,11 +96,8 @@ function GUI(props: GUIProps) {            name: "Welcome to Continue",            hide: false,            description: `- Highlight code and ask a question or give instructions -- Use \`cmd+k\` (Mac) / \`ctrl+k\` (Windows) to open Continue -- Use \`cmd+shift+e\` / \`ctrl+shift+e\` to open file Explorer -- Add your own OpenAI API key to VS Code Settings with \`cmd+,\` -- Use slash commands when you want fine-grained control -- Past steps are included as part of the context by default`, +          - Use \`cmd+m\` (Mac) / \`ctrl+m\` (Windows) to open Continue +          - Use \`/help\` to ask questions about how to use Continue`,            system_message: null,            chat_context: [],            manage_own_chat_context: false, @@ -140,13 +138,14 @@ function GUI(props: GUIProps) {    useEffect(() => {      const listener = (e: any) => {        // Cmd + i to toggle fast model -      if (e.key === "i" && e.metaKey && e.shiftKey) { +      if (e.key === "i" && isMetaEquivalentKeyPressed(e) && e.shiftKey) {          setUsingFastModel((prev) => !prev);          // Cmd + backspace to stop currently running step        } else if (          e.key === "Backspace" && -        e.metaKey && -        typeof history?.current_index !== "undefined" +        isMetaEquivalentKeyPressed(e) && +        typeof history?.current_index !== "undefined" && +        history.timeline[history.current_index]?.active        ) {          client?.deleteAtIndex(history.current_index);        } @@ -169,6 +168,7 @@ function GUI(props: GUIProps) {        const waitingForSteps =          state.active &&          state.history.current_index < state.history.timeline.length && +        state.history.timeline[state.history.current_index] &&          state.history.timeline[            state.history.current_index          ].step.description?.trim() === ""; @@ -221,7 +221,7 @@ function GUI(props: GUIProps) {      if (mainTextInputRef.current) {        let input = (mainTextInputRef.current as any).inputValue;        // cmd+enter to /edit -      if (event?.metaKey) { +      if (isMetaEquivalentKeyPressed(event)) {          input = `/edit ${input}`;        }        (mainTextInputRef.current as any).setInputValue(""); @@ -235,14 +235,14 @@ function GUI(props: GUIProps) {          history.current_index < history.timeline.length        ) {          if ( -          history.timeline[history.current_index].step.name === +          history.timeline[history.current_index]?.step.name ===            "Waiting for user input"          ) {            if (input.trim() === "") return;            onStepUserInput(input, history!.current_index);            return;          } else if ( -          history.timeline[history.current_index].step.name === +          history.timeline[history.current_index]?.step.name ===            "Waiting for user confirmation"          ) {            onStepUserInput("ok", history!.current_index); @@ -260,14 +260,13 @@ function GUI(props: GUIProps) {    const onStepUserInput = (input: string, index: number) => {      if (!client) return; -    console.log("Sending step user input", input, index);      client.sendStepUserInput(input, index);    };    // const iterations = useSelector(selectIterations);    return (      <> -      <LoadingCover hidden={true} message="Downloading local model..." /> +      <Onboarding />        <TextDialog          showDialog={showFeedbackDialog}          onEnter={(text) => { @@ -278,7 +277,7 @@ function GUI(props: GUIProps) {            setShowFeedbackDialog(false);          }}          message={feedbackDialogMessage} -      ></TextDialog> +      />        <TopGUIDiv          ref={topGuiDivRef} @@ -348,12 +347,6 @@ function GUI(props: GUIProps) {          </div>          <ComboBox -          // disabled={ -          //   history?.timeline.length -          //     ? history.timeline[history.current_index].step.name === -          //       "Waiting for user confirmation" -          //     : false -          // }            ref={mainTextInputRef}            onEnter={(e) => {              onMainTextInput(e); @@ -438,7 +431,7 @@ function GUI(props: GUIProps) {              if (!usingFastModel) {                // Show the dialog                setFeedbackDialogMessage( -                "We don't yet support local models, but we're working on it! If privacy is a concern of yours, please use the feedback button in the bottom right to let us know." +                "We don't yet support local models, but we're working on it! If privacy is a concern of yours, please write a short note to let us know."                );                setShowFeedbackDialog(true);              } diff --git a/extension/react-app/src/tabs/additionalContext.tsx b/extension/react-app/src/tabs/additionalContext.tsx deleted file mode 100644 index 98fce9f1..00000000 --- a/extension/react-app/src/tabs/additionalContext.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 3543dd93..00000000 --- a/extension/react-app/src/tabs/chat/MessageDiv.tsx +++ /dev/null @@ -1,75 +0,0 @@ -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-y: 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} children={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 deleted file mode 100644 index a93ad4f9..00000000 --- a/extension/react-app/src/tabs/chat/index.tsx +++ /dev/null @@ -1,267 +0,0 @@ -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 deleted file mode 100644 index a8b3300d..00000000 --- a/extension/react-app/src/tabs/main.tsx +++ /dev/null @@ -1,189 +0,0 @@ -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/welcome.tsx b/extension/react-app/src/tabs/welcome.tsx deleted file mode 100644 index c29d260a..00000000 --- a/extension/react-app/src/tabs/welcome.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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; diff --git a/extension/react-app/src/util/api.ts b/extension/react-app/src/util/api.ts deleted file mode 100644 index bdec1d20..00000000 --- a/extension/react-app/src/util/api.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { -  Configuration, -  DebugApi, -  UnittestApi, -  ChatApi, -} from "../../../src/client"; -import { useSelector } from "react-redux"; -import { useEffect, useState } from "react"; -import { RootStore } from "../redux/store"; - -export function useApi() { -  const apiUrl = useSelector((state: RootStore) => state.config.apiUrl); -  const vscMachineId = useSelector( -    (state: RootStore) => state.config.vscMachineId -  ); -  const [debugApi, setDebugApi] = useState<DebugApi>(); -  const [unittestApi, setUnittestApi] = useState<UnittestApi>(); -  const [chatApi, setChatApi] = useState<ChatApi>(); - -  useEffect(() => { -    if (apiUrl && vscMachineId) { -      let config = new Configuration({ -        basePath: apiUrl, -        fetchApi: fetch, -        middleware: [ -          { -            pre: async (context) => { -              context.init.headers = { -                ...context.init.headers, -                "x-vsc-machine-id": vscMachineId, -              }; -            }, -          }, -        ], -      }); -      setDebugApi(new DebugApi(config)); -      setUnittestApi(new UnittestApi(config)); -      setChatApi(new ChatApi(config)); -    } -  }, [apiUrl, vscMachineId]); - -  return { debugApi, unittestApi, chatApi }; -} diff --git a/extension/react-app/src/util/editCache.ts b/extension/react-app/src/util/editCache.ts deleted file mode 100644 index b8071127..00000000 --- a/extension/react-app/src/util/editCache.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useApi } from "../util/api"; -import { FileEdit, SerializedDebugContext } from "../../../src/client"; -import { useCallback, useEffect, useState } from "react"; - -export function useEditCache() { -  const { debugApi } = useApi(); - -  const fetchNewEdit = useCallback( -    async (debugContext: SerializedDebugContext) => { -      return ( -        await debugApi?.editEndpointDebugEditPost({ -          serializedDebugContext: debugContext, -        }) -      )?.completion; -    }, -    [debugApi] -  ); - -  const [editCache, setEditCache] = useState(new EditCache(fetchNewEdit)); - -  useEffect(() => { -    setEditCache(new EditCache(fetchNewEdit)); -  }, [fetchNewEdit]); - -  return editCache; -} - -/** - * Stores preloaded edits, invalidating based off of debug context changes - */ -class EditCache { -  private _lastDebugContext: SerializedDebugContext | undefined; -  private _cachedEdits: FileEdit[] | undefined; -  private _fetchNewEdit: ( -    debugContext: SerializedDebugContext -  ) => Promise<FileEdit[] | undefined>; -  private _debounceTimer: NodeJS.Timeout | undefined; - -  private _debugContextChanged(debugContext: SerializedDebugContext): boolean { -    if (!this._lastDebugContext) { -      return true; -    } - -    return ( -      JSON.stringify(this._lastDebugContext) !== JSON.stringify(debugContext) -    ); -  } - -  private _debugContextComplete(debugContext: SerializedDebugContext): boolean { -    return debugContext.rangesInFiles.length > 0; -  } - -  public async preloadEdit(debugContext: SerializedDebugContext) { -    if (this._debounceTimer) { -      clearTimeout(this._debounceTimer); -    } -    if ( -      this._debugContextComplete(debugContext) && -      this._debugContextChanged(debugContext) -    ) { -      this._debounceTimer = setTimeout(async () => { -        console.log("Preloading edits"); -        this._cachedEdits = await this._fetchNewEdit(debugContext); -        this._lastDebugContext = debugContext; -      }, 200); -    } -  } - -  public async getEdit( -    debugContext: SerializedDebugContext -  ): Promise<FileEdit[]> { -    if (this._debugContextChanged(debugContext)) { -      console.log("Cache miss"); -      this._cachedEdits = await this._fetchNewEdit(debugContext); -    } else { -      console.log("Cache hit"); -    } - -    return this._cachedEdits!; -  } - -  constructor( -    fetchNewEdit: ( -      debugContext: SerializedDebugContext -    ) => Promise<FileEdit[] | undefined> -  ) { -    this._fetchNewEdit = fetchNewEdit; -  } -} diff --git a/extension/react-app/src/util/index.ts b/extension/react-app/src/util/index.ts index 458f9d95..c4168e13 100644 --- a/extension/react-app/src/util/index.ts +++ b/extension/react-app/src/util/index.ts @@ -1,27 +1,43 @@ -import { RangeInFile } from "../../../src/client"; +type Platform = "mac" | "linux" | "windows" | "unknown"; -export function readRangeInVirtualFileSystem( -  rangeInFile: RangeInFile, -  filesystem: { [filepath: string]: string } -): string | undefined { -  const range = rangeInFile.range; - -  let data = filesystem[rangeInFile.filepath]; -  if (data === undefined) { -    console.log("File not found"); -    return undefined; +export function getPlatform(): Platform { +  const platform = window.navigator.platform.toUpperCase(); +  if (platform.indexOf("MAC") >= 0) { +    return "mac"; +  } else if (platform.indexOf("LINUX") >= 0) { +    return "linux"; +  } else if (platform.indexOf("WIN") >= 0) { +    return "windows";    } else { -    let lines = data.toString().split("\n"); -    if (range.start.line === range.end.line) { -      return lines[rangeInFile.range.start.line].slice( -        rangeInFile.range.start.character, -        rangeInFile.range.end.character -      ); -    } else { -      let firstLine = lines[range.start.line].slice(range.start.character); -      let lastLine = lines[range.end.line].slice(0, range.end.character); -      let middleLines = lines.slice(range.start.line + 1, range.end.line); -      return [firstLine, ...middleLines, lastLine].join("\n"); -    } +    return "unknown"; +  } +} + +export function isMetaEquivalentKeyPressed(event: { +  metaKey: boolean; +  ctrlKey: boolean; +}): boolean { +  const platform = getPlatform(); +  switch (platform) { +    case "mac": +      return event.metaKey; +    case "linux": +    case "windows": +      return event.ctrlKey; +    default: +      return event.metaKey; +  } +} + +export function getMetaKeyLabel(): string { +  const platform = getPlatform(); +  switch (platform) { +    case "mac": +      return "⌘"; +    case "linux": +    case "windows": +      return "^"; +    default: +      return "⌘";    }  } | 
