diff options
| author | Nate Sesti <sestinj@gmail.com> | 2023-07-24 01:00:42 -0700 | 
|---|---|---|
| committer | Nate Sesti <sestinj@gmail.com> | 2023-07-24 01:00:42 -0700 | 
| commit | 85ce06beb9b2d587b0b572117a98318d226bed61 (patch) | |
| tree | 76fc6232b5219d0bd61b547b26624641a99e7b9b /extension/react-app/src | |
| parent | 699a74250fd4cf91af930ff63077aeb81f74856f (diff) | |
| parent | 885f88af1d7b35e03b1de4df3e74a60da1a777ed (diff) | |
| download | sncontinue-85ce06beb9b2d587b0b572117a98318d226bed61.tar.gz sncontinue-85ce06beb9b2d587b0b572117a98318d226bed61.tar.bz2 sncontinue-85ce06beb9b2d587b0b572117a98318d226bed61.zip | |
Merge branch 'main' into show-react-immediately
Diffstat (limited to 'extension/react-app/src')
| -rw-r--r-- | extension/react-app/src/App.tsx | 8 | ||||
| -rw-r--r-- | extension/react-app/src/components/ComboBox.tsx | 102 | ||||
| -rw-r--r-- | extension/react-app/src/components/InputAndButton.tsx | 10 | ||||
| -rw-r--r-- | extension/react-app/src/components/Onboarding.tsx | 19 | ||||
| -rw-r--r-- | extension/react-app/src/components/PillButton.tsx | 172 | ||||
| -rw-r--r-- | extension/react-app/src/components/StepContainer.tsx | 116 | ||||
| -rw-r--r-- | extension/react-app/src/components/TextDialog.tsx | 71 | ||||
| -rw-r--r-- | extension/react-app/src/components/index.ts | 23 | ||||
| -rw-r--r-- | extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts | 35 | ||||
| -rw-r--r-- | extension/react-app/src/hooks/ContinueGUIClientProtocol.ts | 93 | ||||
| -rw-r--r-- | extension/react-app/src/hooks/useContinueGUIProtocol.ts | 91 | ||||
| -rw-r--r-- | extension/react-app/src/hooks/useWebsocket.ts | 2 | ||||
| -rw-r--r-- | extension/react-app/src/index.css | 4 | ||||
| -rw-r--r-- | extension/react-app/src/pages/gui.tsx | 84 | ||||
| -rw-r--r-- | extension/react-app/src/util/index.ts | 43 | 
15 files changed, 482 insertions, 391 deletions
| diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index c9bd42e0..aa462171 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -2,7 +2,7 @@ import DebugPanel from "./components/DebugPanel";  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 @@ -13,11 +13,7 @@ function App() {    return (      <GUIClientContext.Provider value={client}> -      <DebugPanel -        tabs={[ -          { element: <GUI />, title: "GUI" } -        ]} -      /> +      <DebugPanel tabs={[{ element: <GUI />, title: "GUI" }]} />      </GUIClientContext.Provider>    );  } diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 7d6541c7..1e2ca135 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; @@ -96,15 +71,15 @@ const Ul = styled.ul<{        : `transform: translateY(${2 * mainInputFontSize}px);`}    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 +95,7 @@ const Li = styled.li<{    selected: boolean;    isLastItem: boolean;  }>` +  background-color: ${vscBackground};    ${({ highlighted }) => highlighted && "background: #ff000066;"}    ${({ selected }) => selected && "font-weight: bold;"}      padding: 0.5rem 0.75rem; @@ -149,10 +125,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 || []    ); @@ -181,6 +153,7 @@ 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") { @@ -241,6 +214,14 @@ 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} @@ -258,15 +239,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 && @@ -276,11 +248,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();                }} @@ -292,7 +264,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. ⌘⏎ to edit." +          placeholder={`Ask a question, give instructions, or type '/' to see slash commands`}            {...getInputProps({              onChange: (e) => {                const target = e.target as HTMLTextAreaElement; @@ -305,6 +277,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. @@ -359,6 +338,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) => ( @@ -378,29 +358,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {        </div>        {highlightedCodeSections.length === 0 &&          (downshiftProps.inputValue?.startsWith("/edit") || -          (metaKeyPressed && downshiftProps.inputValue?.length > 0)) && ( +          (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); -        }} -        onMouseLeave={() => { -          setHoveringContextDropdown(false); -        }} -        hidden={true || (!hoveringContextDropdown && !hoveringButton)} -      > -        {highlightedCodeSections.map((section, idx) => ( -          <> -            <p>{section.display_name}</p> -            <CodeBlock showCopy={false} key={idx}> -              {section.range.contents} -            </CodeBlock> -          </> -        ))} -      </ContextDropdown>      </>    );  }); 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/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx index 7772a25e..231c1e93 100644 --- a/extension/react-app/src/components/Onboarding.tsx +++ b/extension/react-app/src/components/Onboarding.tsx @@ -22,22 +22,17 @@ const StyledSpan = styled.span`    &:hover {      background-color: #ffffff33;    } +  white-space: nowrap;  `;  const Onboarding = () => {    const [counter, setCounter] = useState(4); -  const gifs = ["intro", "explain", "edit", "generate", "intro"]; +  const gifs = ["intro", "highlight", "question", "help"];    const topMessages = [ -    "Welcome to Continue!", -    "Answer coding questions", -    "Edit in natural language", -    "Generate files from scratch", -  ]; -  const bottomMessages = [ -    "", -    "Ask Continue about a part of your code to get another perspective", -    "Highlight a section of code and instruct Continue to refactor it", -    "Let Continue build the scaffolding of Python scripts, React components, and more", +    "Welcome!", +    "Highlight code", +    "Ask a question", +    "Use /help to learn more",    ];    useEffect(() => { @@ -107,7 +102,6 @@ const Onboarding = () => {              />            )}          </div> -        <p>{bottomMessages[counter]}</p>          <p            style={{              paddingLeft: "50px", @@ -115,6 +109,7 @@ const Onboarding = () => {              paddingBottom: "50px",              textAlign: "center",              cursor: "pointer", +            whiteSpace: "nowrap",            }}          >            <StyledSpan diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index 31d98c0f..5929d06a 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -3,15 +3,20 @@ import styled from "styled-components";  import {    StyledTooltip,    defaultBorderRadius, -  lightGray,    secondaryDark, +  vscBackground, +  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 +33,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 +51,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 +73,8 @@ interface PillButtonProps {    index: number;    editing: boolean;    pinned: boolean; +  warning?: string; +  onlyShowDelete?: boolean;  }  const PillButton = (props: PillButtonProps) => { @@ -63,75 +83,103 @@ 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", +                backgroundColor: vscBackground,                }}              > -              <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 d1a8a46a..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..9597b578 100644 --- a/extension/react-app/src/components/TextDialog.tsx +++ b/extension/react-app/src/components/TextDialog.tsx @@ -1,7 +1,9 @@  // 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"; +import { ReactMarkdown } from "react-markdown/lib/react-markdown";  const ScreenCover = styled.div`    position: absolute; @@ -20,13 +22,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 +39,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;  `; @@ -53,6 +57,7 @@ const TextDialog = (props: {    onEnter: (text: string) => void;    onClose: () => void;    message?: string; +  entryOn?: boolean;  }) => {    const [text, setText] = useState("");    const textAreaRef = React.createRef<HTMLTextAreaElement>(); @@ -76,29 +81,37 @@ const TextDialog = (props: {          }}        >          <Dialog> -          <P>{props.message || ""}</P> -          <TextArea -            rows={10} -            ref={textAreaRef} -            onKeyDown={(e) => { -              if (e.key === "Enter" && e.metaKey && textAreaRef.current) { -                props.onEnter(textAreaRef.current.value); -                setText(""); -              } else if (e.key === "Escape") { -                props.onClose(); -              } -            }} -          /> -          <Button -            onClick={() => { -              if (textAreaRef.current) { -                props.onEnter(textAreaRef.current.value); -                setText(""); -              } -            }} -          > -            Enter -          </Button> +          <ReactMarkdown>{props.message || ""}</ReactMarkdown> +          {props.entryOn && ( +            <> +              <TextArea +                rows={10} +                ref={textAreaRef} +                onKeyDown={(e) => { +                  if ( +                    e.key === "Enter" && +                    isMetaEquivalentKeyPressed(e) && +                    textAreaRef.current +                  ) { +                    props.onEnter(textAreaRef.current.value); +                    setText(""); +                  } else if (e.key === "Escape") { +                    props.onClose(); +                  } +                }} +              /> +              <Button +                onClick={() => { +                  if (textAreaRef.current) { +                    props.onEnter(textAreaRef.current.value); +                    setText(""); +                  } +                }} +              > +                Enter +              </Button> +            </> +          )}          </Dialog>        </DialogContainer>      </ScreenCover> 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/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/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/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index 4ff260fa..49f41dcf 100644 --- a/extension/react-app/src/pages/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"; @@ -23,6 +27,7 @@ import { RootStore } from "../redux/store";  import { postVscMessage } from "../vscode";  import UserInputContainer from "../components/UserInputContainer";  import Onboarding from "../components/Onboarding"; +import { isMetaEquivalentKeyPressed } from "../util";  const TopGUIDiv = styled.div`    overflow: hidden; @@ -70,7 +75,6 @@ function GUI(props: GUIProps) {      }    }, [dataSwitchOn]); -  const [usingFastModel, setUsingFastModel] = useState(false);    const [waitingForSteps, setWaitingForSteps] = useState(false);    const [userInputQueue, setUserInputQueue] = useState<string[]>([]);    const [highlightedRanges, setHighlightedRanges] = useState< @@ -95,11 +99,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, @@ -115,6 +116,7 @@ function GUI(props: GUIProps) {    const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);    const [feedbackDialogMessage, setFeedbackDialogMessage] = useState(""); +  const [feedbackEntryOn, setFeedbackEntryOn] = useState(true);    const topGuiDivRef = useRef<HTMLDivElement>(null); @@ -138,14 +140,11 @@ function GUI(props: GUIProps) {    }, [topGuiDivRef.current, scrollTimeout]);    useEffect(() => { +    // Cmd + Backspace to delete current step      const listener = (e: any) => { -      // Cmd + i to toggle fast model -      if (e.key === "i" && e.metaKey && e.shiftKey) { -        setUsingFastModel((prev) => !prev); -        // Cmd + backspace to stop currently running step -      } else if ( +      if (          e.key === "Backspace" && -        e.metaKey && +        isMetaEquivalentKeyPressed(e) &&          typeof history?.current_index !== "undefined" &&          history.timeline[history.current_index]?.active        ) { @@ -162,7 +161,6 @@ function GUI(props: GUIProps) {    useEffect(() => {      client?.onStateUpdate((state: FullState) => {        // Scroll only if user is at very bottom of the window. -      setUsingFastModel(state.default_model === "gpt-3.5-turbo");        const shouldScrollToBottom =          topGuiDivRef.current &&          topGuiDivRef.current?.offsetHeight - window.scrollY < 100; @@ -223,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(""); @@ -269,15 +267,18 @@ function GUI(props: GUIProps) {    return (      <>        <Onboarding /> -      <TextDialog showDialog={showFeedbackDialog} -      onEnter={(text) => { -        client?.sendMainInput(`/feedback ${text}`); -        setShowFeedbackDialog(false); -      }} -      onClose={() => { -        setShowFeedbackDialog(false); -      }} -      message={feedbackDialogMessage} /> +      <TextDialog +        showDialog={showFeedbackDialog} +        onEnter={(text) => { +          client?.sendMainInput(`/feedback ${text}`); +          setShowFeedbackDialog(false); +        }} +        onClose={() => { +          setShowFeedbackDialog(false); +        }} +        message={feedbackDialogMessage} +        entryOn={feedbackEntryOn} +      />        <TopGUIDiv          ref={topGuiDivRef} @@ -307,6 +308,7 @@ function GUI(props: GUIProps) {              )            ) : (              <StepContainer +              index={index}                isLast={index === history.timeline.length - 1}                isFirst={index === 0}                open={stepsOpen[index]} @@ -371,12 +373,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}        > @@ -425,24 +428,26 @@ function GUI(props: GUIProps) {          </div>          <HeaderButtonWithText            onClick={() => { -            // client?.changeDefaultModel( -            //   usingFastModel ? "gpt-4" : "gpt-3.5-turbo" -            // ); -            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 write a short note to let us know." -              ); -              setShowFeedbackDialog(true); -            } -            setUsingFastModel((prev) => !prev); +            // Show the dialog +            setFeedbackDialogMessage( +              `Continue uses GPT-4 by default, but works with any model. If you'd like to keep your code completely private, there are few options: + +Run a local model with ggml: [5 minute quickstart](https://github.com/continuedev/ggml-server-example) + +Use Azure OpenAI service, which is GDPR and HIPAA compliant: [Tutorial](https://continue.dev/docs/customization#azure-openai-service) + +If you already have an LLM deployed on your own infrastructure, or would like to do so, please contact us at hi@continue.dev. +              ` +            ); +            setFeedbackEntryOn(false); +            setShowFeedbackDialog(true);            }} -          text={usingFastModel ? "local" : "gpt-4"} +          text={"Use Private Model"}          >            <div              style={{ fontSize: "18px", marginLeft: "2px", marginRight: "2px" }}            > -            {usingFastModel ? "🔒" : "🧠"} +            🔒            </div>          </HeaderButtonWithText>          <HeaderButtonWithText @@ -467,6 +472,7 @@ function GUI(props: GUIProps) {              setFeedbackDialogMessage(                "Having trouble using Continue? Want a new feature? Let us know! This box is anonymous, but we will promptly address your feedback."              ); +            setFeedbackEntryOn(true);              setShowFeedbackDialog(true);            }}            text="Feedback" diff --git a/extension/react-app/src/util/index.ts b/extension/react-app/src/util/index.ts new file mode 100644 index 00000000..c4168e13 --- /dev/null +++ b/extension/react-app/src/util/index.ts @@ -0,0 +1,43 @@ +type Platform = "mac" | "linux" | "windows" | "unknown"; + +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 { +    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 "⌘"; +  } +} | 
