diff options
Diffstat (limited to 'extension/react-app/src')
30 files changed, 585 insertions, 1507 deletions
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index 8785f88f..aa462171 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -1,11 +1,8 @@ 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"; +import ContinueGUIClientProtocol from "./hooks/ContinueGUIClientProtocol"; export const GUIClientContext = createContext< ContinueGUIClientProtocol | undefined @@ -16,17 +13,7 @@ function App() { return ( <GUIClientContext.Provider value={client}> - <DebugPanel - tabs={[ - { - element: <GUI />, - title: "GUI", - }, - // { element: <MainTab />, title: "Debug Panel" }, - // { element: <WelcomeTab />, title: "Welcome" }, - // { element: <ChatTab />, title: "Chat" }, - ]} - /> + <DebugPanel tabs={[{ 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.woff2 Binary files differdeleted 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.woff2 Binary files differdeleted 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 e6632360..f327e3a3 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -1,30 +1,20 @@ -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, + vscForeground, } 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; @@ -48,21 +38,6 @@ const EmptyPillDiv = styled.div` } `; -const ContextDropdown = styled.div` - position: absolute; - padding: 4px; - width: calc(100% - 16px - 8px); - background-color: ${secondaryDark}; - color: white; - border-bottom-right-radius: ${defaultBorderRadius}; - border-bottom-left-radius: ${defaultBorderRadius}; - /* border: 1px solid white; */ - border-top: none; - margin: 8px; - outline: 1px solid orange; - z-index: 5; -`; - const MainTextInput = styled.textarea` resize: none; @@ -74,7 +49,7 @@ const MainTextInput = styled.textarea` height: auto; width: 100%; background-color: ${secondaryDark}; - color: white; + color: ${vscForeground}; z-index: 1; border: 1px solid transparent; @@ -97,14 +72,15 @@ const Ul = styled.ul<{ position: absolute; background: ${vscBackground}; background-color: ${secondaryDark}; - color: white; + color: ${vscForeground}; max-height: ${UlMaxHeight}px; + width: calc(100% - 16px); overflow-y: scroll; overflow-x: hidden; padding: 0; ${({ hidden }) => hidden && "display: none;"} border-radius: ${defaultBorderRadius}; - border: 0.5px solid gray; + outline: 0.5px solid gray; z-index: 2; // Get rid of scrollbar and its padding scrollbar-width: none; @@ -120,6 +96,7 @@ const Li = styled.li<{ selected: boolean; isLastItem: boolean; }>` + background-color: ${secondaryDark}; ${({ highlighted }) => highlighted && "background: #ff000066;"} ${({ selected }) => selected && "font-weight: bold;"} padding: 0.5rem 0.75rem; @@ -149,10 +126,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // The position of the current command you are typing now, so the one that will be appended to history once you press enter const [positionInHistory, setPositionInHistory] = React.useState<number>(0); const [items, setItems] = React.useState(props.items); - const [hoveringButton, setHoveringButton] = React.useState(false); - const [hoveringContextDropdown, setHoveringContextDropdown] = - React.useState(false); - const [pinned, setPinned] = useState(false); const [highlightedCodeSections, setHighlightedCodeSections] = React.useState( props.highlightedCodeSections || [] ); @@ -180,6 +153,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,11 +215,19 @@ 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 + } + onlyShowDelete={ + highlightedCodeSections.length <= 1 || section.editing + } editing={section.editing} pinned={section.pinned} index={idx} - key={`${section.filepath}${idx}`} - title={`${section.range.filepath} (${ + key={`${section.display_name}${idx}`} + title={`${section.display_name} (${ section.range.range.start.line + 1 }-${section.range.range.end.line + 1})`} onDelete={() => { @@ -238,15 +240,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { return newSections; }); }} - onHover={(val: boolean) => { - if (val) { - setHoveringButton(val); - } else { - setTimeout(() => { - setHoveringButton(val); - }, 100); - } - }} /> ))} {props.highlightedCodeSections.length > 0 && @@ -256,11 +249,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { props.onToggleAddContext(); }} > - Highlight to Add Context + Highlight code section </EmptyPillDiv> ) : ( <HeaderButtonWithText - text="Add to Context" + text="Add more code to context" onClick={() => { props.onToggleAddContext(); }} @@ -272,7 +265,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`} {...getInputProps({ onChange: (e) => { const target = e.target as HTMLTextAreaElement; @@ -285,6 +278,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 +311,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 && @@ -340,6 +339,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { })} showAbove={showAbove()} ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} + hidden={!downshiftProps.isOpen || items.length === 0} > {downshiftProps.isOpen && items.map((item, index) => ( @@ -357,28 +357,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> */} - <ContextDropdown - onMouseEnter={() => { - setHoveringContextDropdown(true); - }} - onMouseLeave={() => { - setHoveringContextDropdown(false); - }} - hidden={true || (!hoveringContextDropdown && !hoveringButton)} - > - {highlightedCodeSections.map((section, idx) => ( - <> - <p>{section.range.filepath}</p> - <CodeBlock showCopy={false} key={idx}> - {section.range.contents} - </CodeBlock> - </> - ))} - </ContextDropdown> + {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> + )} </> ); }); diff --git a/extension/react-app/src/components/InputAndButton.tsx b/extension/react-app/src/components/InputAndButton.tsx index 0a8592f2..8019d014 100644 --- a/extension/react-app/src/components/InputAndButton.tsx +++ b/extension/react-app/src/components/InputAndButton.tsx @@ -1,6 +1,6 @@ import React, { useRef } from "react"; import styled from "styled-components"; -import { vscBackground } from "."; +import { vscBackground, vscForeground } from "."; interface InputAndButtonProps { onUserInput: (input: string) => void; @@ -16,7 +16,7 @@ const Input = styled.input` padding: 0.5rem; border: 1px solid white; background-color: ${vscBackground}; - color: white; + color: ${vscForeground}; border-radius: 4px; border-top-right-radius: 0; border-bottom-right-radius: 0; @@ -27,7 +27,7 @@ const Button = styled.button` padding: 0.5rem; border: 1px solid white; background-color: ${vscBackground}; - color: white; + color: ${vscForeground}; border-radius: 4px; border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -35,8 +35,8 @@ const Button = styled.button` cursor: pointer; &:hover { - background-color: white; - color: black; + background-color: ${vscForeground}; + color: ${vscBackground}; } `; 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..c24dba83 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -3,15 +3,19 @@ import styled from "styled-components"; import { StyledTooltip, defaultBorderRadius, - lightGray, secondaryDark, + vscForeground, } from "."; -import { Trash, PaintBrush, MapPin } from "@styled-icons/heroicons-outline"; +import { + Trash, + PaintBrush, + ExclamationTriangle, +} from "@styled-icons/heroicons-outline"; import { GUIClientContext } from "../App"; const Button = styled.button` border: none; - color: white; + color: ${vscForeground}; background-color: ${secondaryDark}; border-radius: ${defaultBorderRadius}; padding: 8px; @@ -28,10 +32,8 @@ const GridDiv = styled.div` height: 100%; display: grid; grid-gap: 0; - grid-template-columns: 1fr 1fr; align-items: center; border-radius: ${defaultBorderRadius}; - overflow: hidden; background-color: ${secondaryDark}; `; @@ -48,6 +50,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 +72,8 @@ interface PillButtonProps { index: number; editing: boolean; pinned: boolean; + warning?: string; + onlyShowDelete?: boolean; } const PillButton = (props: PillButtonProps) => { @@ -63,75 +82,102 @@ 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]); + <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 + style={{ + gridTemplateColumns: props.onlyShowDelete ? "1fr" : "1fr 1fr", }} > - <PaintBrush style={{ margin: "auto" }} width="1.6em"></PaintBrush> - </ButtonDiv> + {props.onlyShowDelete || ( + <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 section (with entire file as context)" + : "Edit this section"} + </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..bc8665fd 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 { useContext, useEffect, useRef, useState } from "react"; import styled, { keyframes } from "styled-components"; import { appear, @@ -6,18 +6,21 @@ import { secondaryDark, vscBackground, vscBackgroundTransparent, + vscForeground, } from "."; import { ChevronDown, ChevronRight, ArrowPath, XMark, + MagnifyingGlass, } 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"; +import { GUIClientContext } from "../App"; interface StepContainerProps { historyNode: HistoryNode; @@ -31,6 +34,7 @@ interface StepContainerProps { onToggle: () => void; isFirst: boolean; isLast: boolean; + index: number; } // #region styled components @@ -38,7 +42,6 @@ interface StepContainerProps { const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean }>` opacity: ${(props) => (props.inFuture ? 0.3 : 1)}; animation: ${appear} 0.3s ease-in-out; - /* padding-left: ${(props) => props.stepDepth * 20}px; */ overflow: hidden; margin-left: 0px; margin-right: 0px; @@ -52,12 +55,7 @@ const StepContainerDiv = styled.div<{ open: boolean }>` `; const HeaderDiv = styled.div<{ error: boolean; loading: boolean }>` - background-color: ${(props) => - props.error - ? "#522" - : props.loading - ? vscBackgroundTransparent - : vscBackground}; + background-color: ${(props) => (props.error ? "#522" : vscBackground)}; display: grid; grid-template-columns: 1fr auto auto; grid-gap: 8px; @@ -72,19 +70,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 +109,33 @@ 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: #f78383; + word-wrap: break-word; + border-radius: ${defaultBorderRadius}; + background-color: ${secondaryDark}; + } + + pre > code { + background-color: ${secondaryDark}; + color: ${vscForeground}; + } + + background-color: ${vscBackground}; + font-family: "Lexend", sans-serif; + font-size: 13px; + padding: 8px; + color: ${vscForeground}; +`; + // #endregion function StepContainer(props: StepContainerProps) { @@ -131,6 +143,7 @@ function StepContainer(props: StepContainerProps) { const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null); const userInputRef = useRef<HTMLInputElement>(null); const isUserInput = props.historyNode.step.name === "UserInputStep"; + const client = useContext(GUIClientContext); useEffect(() => { if (userInputRef?.current) { @@ -158,7 +171,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 +183,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 +191,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"> @@ -201,12 +214,27 @@ function StepContainer(props: StepContainerProps) { </HeaderButton> */} <> + {(props.historyNode.logs as any)?.length > 0 && ( + <HeaderButtonWithText + text="Logs" + onClick={(e) => { + e.stopPropagation(); + client?.showLogsAtIndex(props.index); + }} + > + <MagnifyingGlass size="1.4em" /> + </HeaderButtonWithText> + )} <HeaderButtonWithText onClick={(e) => { 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 +270,19 @@ 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} />; - }, + <StyledMarkdownPreview + source={props.historyNode.step.description || ""} + wrapperElement={{ + "data-color-mode": "dark", }} - > - {props.historyNode.step.description as any} - </ReactMarkdown> + /> )} </ContentDiv> </StepContainerDiv> diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx index ea5727f0..cba3852d 100644 --- a/extension/react-app/src/components/TextDialog.tsx +++ b/extension/react-app/src/components/TextDialog.tsx @@ -1,7 +1,8 @@ // Write a component that displays a dialog box with a text field and a button. import React, { useEffect, useState } from "react"; import styled from "styled-components"; -import { Button, buttonColor, secondaryDark, vscBackground } from "."; +import { Button, secondaryDark, vscBackground, vscForeground } from "."; +import { isMetaEquivalentKeyPressed } from "../util"; const ScreenCover = styled.div` position: absolute; @@ -20,13 +21,13 @@ const DialogContainer = styled.div` `; const Dialog = styled.div` - background-color: white; + color: ${vscForeground}; + background-color: ${vscBackground}; border-radius: 8px; padding: 8px; display: flex; flex-direction: column; - /* box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.5); */ - border: 2px solid ${buttonColor}; + box-shadow: 0 0 10px 0 ${vscForeground}; width: fit-content; margin: auto; `; @@ -37,14 +38,16 @@ const TextArea = styled.textarea` padding: 8px; outline: 1px solid black; resize: none; + background-color: ${secondaryDark}; + color: ${vscForeground}; &:focus { - outline: 1px solid ${buttonColor}; + outline: 1px solid ${vscForeground}; } `; const P = styled.p` - color: black; + color: ${vscForeground}; margin: 8px auto; `; @@ -81,7 +84,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/components/index.ts b/extension/react-app/src/components/index.ts index 9ae0f097..cb5e7915 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -3,12 +3,16 @@ import styled, { keyframes } from "styled-components"; export const defaultBorderRadius = "5px"; export const lightGray = "rgb(100 100 100)"; -export const secondaryDark = "rgb(45 45 45)"; -export const vscBackground = "rgb(30 30 30)"; +// export const secondaryDark = "rgb(45 45 45)"; +// export const vscBackground = "rgb(30 30 30)"; export const vscBackgroundTransparent = "#1e1e1ede"; export const buttonColor = "rgb(113 28 59)"; export const buttonColorHover = "rgb(113 28 59 0.67)"; +export const secondaryDark = "var(--vscode-textBlockQuote-background)"; +export const vscBackground = "var(--vscode-editor-background)"; +export const vscForeground = "var(--vscode-editor-foreground)"; + export const Button = styled.button` padding: 10px 12px; margin: 8px 0; @@ -46,8 +50,8 @@ export const TextArea = styled.textarea` resize: vertical; padding: 4px; - caret-color: white; - color: white; + caret-color: ${vscForeground}; + color: #{vscForeground}; &:focus { outline: 1px solid ${buttonColor}; @@ -120,7 +124,7 @@ export const MainTextInput = styled.textarea` border: 1px solid #ccc; margin: 8px 8px; background-color: ${vscBackground}; - color: white; + color: ${vscForeground}; outline: 1px solid orange; resize: none; `; @@ -137,8 +141,9 @@ export const appear = keyframes` `; export const HeaderButton = styled.button<{ inverted: boolean | undefined }>` - background-color: ${({ inverted }) => (inverted ? "white" : "transparent")}; - color: ${({ inverted }) => (inverted ? "black" : "white")}; + background-color: ${({ inverted }) => + inverted ? vscForeground : "transparent"}; + color: ${({ inverted }) => (inverted ? vscBackground : vscForeground)}; border: none; border-radius: ${defaultBorderRadius}; @@ -146,7 +151,9 @@ export const HeaderButton = styled.button<{ inverted: boolean | undefined }>` &:hover { background-color: ${({ inverted }) => - typeof inverted === "undefined" || inverted ? lightGray : "transparent"}; + typeof inverted === "undefined" || inverted + ? secondaryDark + : "transparent"}; } display: flex; align-items: center; 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/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts new file mode 100644 index 00000000..6c0df8fc --- /dev/null +++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts @@ -0,0 +1,35 @@ +abstract class AbstractContinueGUIClientProtocol { + abstract sendMainInput(input: string): void; + + abstract reverseToIndex(index: number): void; + + abstract sendRefinementInput(input: string, index: number): void; + + abstract sendStepUserInput(input: string, index: number): void; + + abstract onStateUpdate(state: any): void; + + abstract onAvailableSlashCommands( + callback: (commands: { name: string; description: string }[]) => void + ): void; + + abstract changeDefaultModel(model: string): void; + + abstract sendClear(): void; + + abstract retryAtIndex(index: number): void; + + abstract deleteAtIndex(index: number): void; + + abstract deleteContextAtIndices(indices: number[]): void; + + abstract setEditingAtIndices(indices: number[]): void; + + abstract setPinnedAtIndices(indices: number[]): void; + + abstract toggleAddingHighlightedCode(): void; + + abstract showLogsAtIndex(index: number): void; +} + +export default AbstractContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index a179c2bf..7d6c2a71 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -1,33 +1,92 @@ -abstract class AbstractContinueGUIClientProtocol { - abstract sendMainInput(input: string): void; +import AbstractContinueGUIClientProtocol from "./AbstractContinueGUIClientProtocol"; +import { Messenger, WebsocketMessenger } from "./messenger"; +import { VscodeMessenger } from "./vscodeMessenger"; - abstract reverseToIndex(index: number): void; +class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { + messenger: Messenger; + // Server URL must contain the session ID param + serverUrlWithSessionId: string; - abstract sendRefinementInput(input: string, index: number): void; + constructor( + serverUrlWithSessionId: string, + useVscodeMessagePassing: boolean + ) { + super(); + this.serverUrlWithSessionId = serverUrlWithSessionId; + this.messenger = useVscodeMessagePassing + ? new VscodeMessenger(serverUrlWithSessionId) + : new WebsocketMessenger(serverUrlWithSessionId); + } - abstract sendStepUserInput(input: string, index: number): void; + sendMainInput(input: string) { + this.messenger.send("main_input", { input }); + } - abstract onStateUpdate(state: any): void; + reverseToIndex(index: number) { + this.messenger.send("reverse_to_index", { index }); + } - abstract onAvailableSlashCommands( + sendRefinementInput(input: string, index: number) { + this.messenger.send("refinement_input", { input, index }); + } + + sendStepUserInput(input: string, index: number) { + this.messenger.send("step_user_input", { input, index }); + } + + onStateUpdate(callback: (state: any) => void) { + this.messenger.onMessageType("state_update", (data: any) => { + if (data.state) { + callback(data.state); + } + }); + } + + onAvailableSlashCommands( callback: (commands: { name: string; description: string }[]) => void - ): void; + ) { + this.messenger.onMessageType("available_slash_commands", (data: any) => { + if (data.commands) { + callback(data.commands); + } + }); + } + + changeDefaultModel(model: string) { + this.messenger.send("change_default_model", { model }); + } - abstract changeDefaultModel(model: string): void; + sendClear() { + this.messenger.send("clear_history", {}); + } - abstract sendClear(): void; + retryAtIndex(index: number) { + this.messenger.send("retry_at_index", { index }); + } - abstract retryAtIndex(index: number): void; + deleteAtIndex(index: number) { + this.messenger.send("delete_at_index", { index }); + } - abstract deleteAtIndex(index: number): void; + deleteContextAtIndices(indices: number[]) { + this.messenger.send("delete_context_at_indices", { indices }); + } - abstract deleteContextAtIndices(indices: number[]): void; + setEditingAtIndices(indices: number[]) { + this.messenger.send("set_editing_at_indices", { indices }); + } - abstract setEditingAtIndices(indices: number[]): void; + setPinnedAtIndices(indices: number[]) { + this.messenger.send("set_pinned_at_indices", { indices }); + } - abstract setPinnedAtIndices(indices: number[]): void; + toggleAddingHighlightedCode(): void { + this.messenger.send("toggle_adding_highlighted_code", {}); + } - abstract toggleAddingHighlightedCode(): void; + showLogsAtIndex(index: number): void { + this.messenger.send("show_logs_at_index", { index }); + } } -export default AbstractContinueGUIClientProtocol; +export default ContinueGUIClientProtocol; 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/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts deleted file mode 100644 index 2060dd7f..00000000 --- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts +++ /dev/null @@ -1,91 +0,0 @@ -import AbstractContinueGUIClientProtocol from "./ContinueGUIClientProtocol"; -// import { Messenger, WebsocketMessenger } from "../../../src/util/messenger"; -import { Messenger, WebsocketMessenger } from "./messenger"; -import { VscodeMessenger } from "./vscodeMessenger"; - -class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { - messenger: Messenger; - // Server URL must contain the session ID param - serverUrlWithSessionId: string; - - constructor( - serverUrlWithSessionId: string, - useVscodeMessagePassing: boolean - ) { - super(); - this.serverUrlWithSessionId = serverUrlWithSessionId; - if (useVscodeMessagePassing) { - this.messenger = new VscodeMessenger(serverUrlWithSessionId); - } else { - this.messenger = new WebsocketMessenger(serverUrlWithSessionId); - } - } - - sendMainInput(input: string) { - this.messenger.send("main_input", { input }); - } - - reverseToIndex(index: number) { - this.messenger.send("reverse_to_index", { index }); - } - - sendRefinementInput(input: string, index: number) { - this.messenger.send("refinement_input", { input, index }); - } - - sendStepUserInput(input: string, index: number) { - this.messenger.send("step_user_input", { input, index }); - } - - onStateUpdate(callback: (state: any) => void) { - this.messenger.onMessageType("state_update", (data: any) => { - if (data.state) { - callback(data.state); - } - }); - } - - onAvailableSlashCommands( - callback: (commands: { name: string; description: string }[]) => void - ) { - this.messenger.onMessageType("available_slash_commands", (data: any) => { - if (data.commands) { - callback(data.commands); - } - }); - } - - changeDefaultModel(model: string) { - this.messenger.send("change_default_model", { model }); - } - - sendClear() { - this.messenger.send("clear_history", {}); - } - - retryAtIndex(index: number) { - this.messenger.send("retry_at_index", { index }); - } - - deleteAtIndex(index: number) { - this.messenger.send("delete_at_index", { index }); - } - - deleteContextAtIndices(indices: number[]) { - this.messenger.send("delete_context_at_indices", { indices }); - } - - setEditingAtIndices(indices: number[]) { - this.messenger.send("set_editing_at_indices", { indices }); - } - - setPinnedAtIndices(indices: number[]) { - this.messenger.send("set_pinned_at_indices", { indices }); - } - - toggleAddingHighlightedCode(): void { - this.messenger.send("toggle_adding_highlighted_code", {}); - } -} - -export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/useWebsocket.ts b/extension/react-app/src/hooks/useWebsocket.ts index e762666f..6b36be97 100644 --- a/extension/react-app/src/hooks/useWebsocket.ts +++ b/extension/react-app/src/hooks/useWebsocket.ts @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { RootStore } from "../redux/store"; import { useSelector } from "react-redux"; -import ContinueGUIClientProtocol from "./useContinueGUIProtocol"; +import ContinueGUIClientProtocol from "./ContinueGUIClientProtocol"; import { postVscMessage } from "../vscode"; function useContinueGUIProtocol(useVscodeMessagePassing: boolean = true) { diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css index 6e33c89c..bac7fe97 100644 --- a/extension/react-app/src/index.css +++ b/extension/react-app/src/index.css @@ -14,13 +14,13 @@ html, body, #root { height: 100%; - background-color: var(--vsc-background); + background-color: var(--vscode-editor-background); font-family: "Lexend", sans-serif; } body { padding: 0; - color: white; + color: var(--vscode-editor-foreground); padding: 0px; margin: 0px; height: 100%; diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/pages/gui.tsx index e1ecec9e..fccc9b4b 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -1,5 +1,9 @@ import styled from "styled-components"; -import { defaultBorderRadius } from "../components"; +import { + defaultBorderRadius, + vscBackground, + vscForeground, +} from "../components"; import Loader from "../components/Loader"; import ContinueButton from "../components/ContinueButton"; import { FullState, HighlightedRangeContext } from "../../../schema/FullState"; @@ -20,9 +24,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 +100,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 +142,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 +172,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 +225,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 +239,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 +264,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 +281,7 @@ function GUI(props: GUIProps) { setShowFeedbackDialog(false); }} message={feedbackDialogMessage} - ></TextDialog> + /> <TopGUIDiv ref={topGuiDivRef} @@ -308,6 +311,7 @@ function GUI(props: GUIProps) { ) ) : ( <StepContainer + index={index} isLast={index === history.timeline.length - 1} isFirst={index === 0} open={stepsOpen[index]} @@ -348,12 +352,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); @@ -378,12 +376,13 @@ function GUI(props: GUIProps) { style={{ position: "fixed", bottom: "50px", - backgroundColor: "white", - color: "black", + backgroundColor: vscBackground, + color: vscForeground, borderRadius: defaultBorderRadius, padding: "16px", margin: "16px", zIndex: 100, + boxShadow: `0px 0px 10px 0px ${vscForeground}`, }} hidden={!showDataSharingInfo} > @@ -438,7 +437,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 "⌘"; } } |