diff options
| author | Nate Sesti <sestinj@gmail.com> | 2023-07-22 22:37:13 -0700 | 
|---|---|---|
| committer | Nate Sesti <sestinj@gmail.com> | 2023-07-22 22:37:13 -0700 | 
| commit | 4d7e72970f770eb49627589fb142c93dfb6fd73b (patch) | |
| tree | 7c85fb17a9e10ac8e387a001f021aa45c8c46582 /extension/react-app/src | |
| parent | 007780d6d60095d4e0b238358ec26b2ec776b73e (diff) | |
| download | sncontinue-4d7e72970f770eb49627589fb142c93dfb6fd73b.tar.gz sncontinue-4d7e72970f770eb49627589fb142c93dfb6fd73b.tar.bz2 sncontinue-4d7e72970f770eb49627589fb142c93dfb6fd73b.zip  | |
@ feature (very large commit)
Diffstat (limited to 'extension/react-app/src')
11 files changed, 296 insertions, 172 deletions
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index f327e3a3..22a6d000 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -1,4 +1,9 @@ -import React, { useEffect, useImperativeHandle, useState } from "react"; +import React, { +  useContext, +  useEffect, +  useImperativeHandle, +  useState, +} from "react";  import { useCombobox } from "downshift";  import styled from "styled-components";  import { @@ -8,13 +13,15 @@ import {    vscBackground,    vscForeground,  } from "."; -import CodeBlock from "./CodeBlock";  import PillButton from "./PillButton";  import HeaderButtonWithText from "./HeaderButtonWithText";  import { DocumentPlus } from "@styled-icons/heroicons-outline"; -import { HighlightedRangeContext } from "../../../schema/FullState"; +import { ContextItem } from "../../../schema/FullState";  import { postVscMessage } from "../vscode"; -import { getMetaKeyLabel } from "../util"; +import { GUIClientContext } from "../App"; +import { MeiliSearch } from "meilisearch"; + +const SEARCH_INDEX_NAME = "continue_context_items";  // #region styled components  const mainInputFontSize = 13; @@ -64,14 +71,14 @@ const Ul = styled.ul<{    hidden: boolean;    showAbove: boolean;    ulHeightPixels: number; +  inputBoxHeight?: string;  }>`    ${(props) =>      props.showAbove        ? `transform: translateY(-${props.ulHeightPixels + 8}px);` -      : `transform: translateY(${2 * mainInputFontSize}px);`} +      : `transform: translateY(${5 * mainInputFontSize}px);`}    position: absolute;    background: ${vscBackground}; -  background-color: ${secondaryDark};    color: ${vscForeground};    max-height: ${UlMaxHeight}px;    width: calc(100% - 16px); @@ -80,15 +87,9 @@ const Ul = styled.ul<{    padding: 0;    ${({ hidden }) => hidden && "display: none;"}    border-radius: ${defaultBorderRadius}; -  outline: 0.5px solid gray; +  outline: 1px solid ${lightGray};    z-index: 2; -  // Get rid of scrollbar and its padding -  scrollbar-width: none;    -ms-overflow-style: none; -  &::-webkit-scrollbar { -    width: 0px; -    background: transparent; /* make scrollbar transparent */ -  }  `;  const Li = styled.li<{ @@ -96,49 +97,90 @@ const Li = styled.li<{    selected: boolean;    isLastItem: boolean;  }>` -  background-color: ${secondaryDark}; -  ${({ highlighted }) => highlighted && "background: #ff000066;"} +  background-color: ${({ highlighted }) => +    highlighted ? lightGray : secondaryDark}; +  ${({ highlighted }) => highlighted && `background: ${vscBackground};`}    ${({ selected }) => selected && "font-weight: bold;"}      padding: 0.5rem 0.75rem;    display: flex;    flex-direction: column;    ${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"} -  border-top: 1px solid gray; +  /* border-top: 1px solid gray; */    cursor: pointer;  `;  // #endregion  interface ComboBoxProps { -  items: { name: string; description: string }[]; +  items: { name: string; description: string; id?: string }[];    onInputValueChange: (inputValue: string) => void;    disabled?: boolean;    onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void; -  highlightedCodeSections: HighlightedRangeContext[]; -  deleteContextItems: (indices: number[]) => void; -  onTogglePin: () => void; +  selectedContextItems: ContextItem[];    onToggleAddContext: () => void;    addingHighlightedCode: boolean;  }  const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { +  const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" }); +  const client = useContext(GUIClientContext); +    const [history, setHistory] = React.useState<string[]>([]);    // 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 [highlightedCodeSections, setHighlightedCodeSections] = React.useState( -    props.highlightedCodeSections || [] -  ); +    const inputRef = React.useRef<HTMLInputElement>(null); +  const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>( +    undefined +  ); -  useEffect(() => { -    setHighlightedCodeSections(props.highlightedCodeSections || []); -  }, [props.highlightedCodeSections]); +  // Whether the current input follows an '@' and should be treated as context query +  const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);    const { getInputProps, ...downshiftProps } = useCombobox({ -    onInputValueChange({ inputValue }) { +    onSelectedItemChange: ({ selectedItem }) => { +      if (selectedItem?.id) { +        // Get the query from the input value +        const segs = downshiftProps.inputValue.split("@"); +        const query = segs[segs.length - 1]; +        const restOfInput = segs.splice(0, segs.length - 1).join("@"); + +        // Tell server the context item was selected +        client?.selectContextItem(selectedItem.id, query); + +        // Remove the '@' and the context query from the input +        if (downshiftProps.inputValue.includes("@")) { +          downshiftProps.setInputValue(restOfInput); +        } +      } +    }, +    onInputValueChange({ inputValue, highlightedIndex }) {        if (!inputValue) return;        props.onInputValueChange(inputValue); + +      if (inputValue.endsWith("@") || currentlyInContextQuery) { +        setCurrentlyInContextQuery(true); + +        const segs = inputValue.split("@"); +        const providerAndQuery = segs[segs.length - 1]; +        const [provider, query] = providerAndQuery.split(" "); +        searchClient +          .index(SEARCH_INDEX_NAME) +          .search(providerAndQuery) +          .then((res) => { +            setItems( +              res.hits.map((hit) => { +                return { +                  name: hit.name, +                  description: hit.description, +                  id: hit.id, +                }; +              }) +            ); +          }); +        return; +      }        setItems(          props.items.filter((item) =>            item.name.toLowerCase().startsWith(inputValue.toLowerCase()) @@ -151,6 +193,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {      },    }); +  useEffect(() => { +    console.log( +      "downshiftProps.highlightedIndex", +      downshiftProps.highlightedIndex +    ); +    if (downshiftProps.highlightedIndex < 0) { +      downshiftProps.setHighlightedIndex(0); +    } +  }, [downshiftProps.inputValue]); +    useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);    const [metaKeyPressed, setMetaKeyPressed] = useState(false); @@ -199,50 +251,22 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {    return (      <>        <div className="px-2 flex gap-2 items-center flex-wrap mt-2"> -        {/* {highlightedCodeSections.length > 1 && ( -          <> -            <HeaderButtonWithText -              text="Clear Context" -              onClick={() => { -                props.deleteContextItems( -                  highlightedCodeSections.map((_, idx) => idx) -                ); -              }} -            > -              <Trash size="1.6em" /> -            </HeaderButtonWithText> -          </> -        )} */} -        {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.display_name}${idx}`} -            title={`${section.display_name} (${ -              section.range.range.start.line + 1 -            }-${section.range.range.end.line + 1})`} -            onDelete={() => { -              if (props.deleteContextItems) { -                props.deleteContextItems([idx]); +        {props.selectedContextItems.map((item, idx) => { +          return ( +            <PillButton +              key={`${item.description.id.item_id}${idx}`} +              item={item} +              warning={ +                item.content.length > 4000 && item.editing +                  ? "Editing such a large range may be slow" +                  : undefined                } -              setHighlightedCodeSections((prev) => { -                const newSections = [...prev]; -                newSections.splice(idx, 1); -                return newSections; -              }); -            }} -          /> -        ))} -        {props.highlightedCodeSections.length > 0 && +              addingHighlightedCode={props.addingHighlightedCode} +              index={idx} +            /> +          ); +        })} +        {props.selectedContextItems.length > 0 &&            (props.addingHighlightedCode ? (              <EmptyPillDiv                onClick={() => { @@ -275,6 +299,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  target.scrollHeight,                  300                ).toString()}px`; +              setInputBoxHeight(target.style.height);                // setShowContextDropdown(target.value.endsWith("@"));              }, @@ -289,6 +314,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                if (event.key === "Enter" && event.shiftKey) {                  // Prevent Downshift's default 'Enter' behavior.                  (event.nativeEvent as any).preventDownshiftDefault = true; +                setCurrentlyInContextQuery(false);                } else if (                  event.key === "Enter" &&                  (!downshiftProps.isOpen || items.length === 0) @@ -302,6 +328,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  (event.nativeEvent as any).preventDownshiftDefault = true;                  if (props.onEnter) props.onEnter(event); +                setCurrentlyInContextQuery(false);                } else if (event.key === "Tab" && items.length > 0) {                  downshiftProps.setInputValue(items[0].name);                  event.preventDefault(); @@ -321,6 +348,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  }                  downshiftProps.setInputValue(history[positionInHistory - 1]);                  setPositionInHistory((prev) => prev - 1); +                setCurrentlyInContextQuery(false);                } else if (event.key === "ArrowDown") {                  if (positionInHistory < history.length) {                    downshiftProps.setInputValue(history[positionInHistory + 1]); @@ -328,6 +356,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  setPositionInHistory((prev) =>                    Math.min(prev + 1, history.length)                  ); +                setCurrentlyInContextQuery(false);                }              },              ref: inputRef, @@ -351,13 +380,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  selected={downshiftProps.selectedItem === item}                >                  <span> -                  {item.name}: {item.description} +                  {item.name}:{"  "} +                  <span style={{ color: lightGray }}>{item.description}</span>                  </span>                </Li>              ))}          </Ul>        </div> -      {highlightedCodeSections.length === 0 && +      {props.selectedContextItems.length === 0 &&          (downshiftProps.inputValue?.startsWith("/edit") ||            (focused &&              metaKeyPressed && diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index c24dba83..af4263af 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -1,9 +1,10 @@ -import { useContext, useState } from "react"; +import { useContext, useEffect, useState } from "react";  import styled from "styled-components";  import {    StyledTooltip,    defaultBorderRadius,    secondaryDark, +  vscBackground,    vscForeground,  } from ".";  import { @@ -12,6 +13,14 @@ import {    ExclamationTriangle,  } from "@styled-icons/heroicons-outline";  import { GUIClientContext } from "../App"; +import { useDispatch } from "react-redux"; +import { +  setBottomMessage, +  setBottomMessageCloseTimeout, +} from "../redux/slices/uiStateSlice"; +import { ContextItem } from "../../../schema/FullState"; +import { ReactMarkdown } from "react-markdown/lib/react-markdown"; +import StyledMarkdownPreview from "./StyledMarkdownPreview";  const Button = styled.button`    border: none; @@ -67,19 +76,48 @@ const CircleDiv = styled.div`  interface PillButtonProps {    onHover?: (arg0: boolean) => void; -  onDelete?: () => void; -  title: string; -  index: number; -  editing: boolean; -  pinned: boolean; +  item: ContextItem;    warning?: string; -  onlyShowDelete?: boolean; +  index: number; +  addingHighlightedCode?: boolean;  }  const PillButton = (props: PillButtonProps) => {    const [isHovered, setIsHovered] = useState(false);    const client = useContext(GUIClientContext); +  const dispatch = useDispatch(); + +  useEffect(() => { +    if (isHovered) { +      dispatch(setBottomMessageCloseTimeout(undefined)); +      dispatch( +        setBottomMessage( +          <> +            <b>{props.item.description.name}</b>:{" "} +            {props.item.description.description} +            <StyledMarkdownPreview +              source={`\`\`\`\n${props.item.content}\n\`\`\``} +              wrapperElement={{ +                "data-color-mode": "dark", +              }} +            /> +          </> +        ) +      ); +    } else { +      dispatch( +        setBottomMessageCloseTimeout( +          setTimeout(() => { +            if (!isHovered) { +              dispatch(setBottomMessage(undefined)); +            } +          }, 2000) +        ) +      ); +    } +  }, [isHovered]); +    return (      <>        <div style={{ position: "relative" }}> @@ -88,10 +126,8 @@ const PillButton = (props: PillButtonProps) => {              position: "relative",              borderColor: props.warning                ? "red" -              : props.editing +              : props.item.editing                ? "#8800aa" -              : props.pinned -              ? "#ffff0099"                : "transparent",              borderWidth: "1px",              borderStyle: "solid", @@ -112,10 +148,14 @@ const PillButton = (props: PillButtonProps) => {            {isHovered && (              <GridDiv                style={{ -                gridTemplateColumns: props.onlyShowDelete ? "1fr" : "1fr 1fr", +                gridTemplateColumns: +                  props.item.editable && props.addingHighlightedCode +                    ? "1fr 1fr" +                    : "1fr", +                backgroundColor: vscBackground,                }}              > -              {props.onlyShowDelete || ( +              {props.item.editable && props.addingHighlightedCode && (                  <ButtonDiv                    data-tooltip-id={`edit-${props.index}`}                    backgroundColor={"#8800aa55"} @@ -130,15 +170,6 @@ const PillButton = (props: PillButtonProps) => {                  </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> @@ -146,33 +177,33 @@ const PillButton = (props: PillButtonProps) => {                  data-tooltip-id={`delete-${props.index}`}                  backgroundColor={"#cc000055"}                  onClick={() => { -                  if (props.onDelete) { -                    props.onDelete(); -                  } +                  client?.deleteContextWithIds([props.item.description.id]);                  }}                >                  <Trash style={{ margin: "auto" }} width="1.6em"></Trash>                </ButtonDiv>              </GridDiv>            )} -          {props.title} +          {props.item.description.name}          </Button>          <StyledTooltip id={`edit-${props.index}`}> -          {props.editing +          {props.item.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}`}> +            <CircleDiv +              data-tooltip-id={`circle-div-${props.item.description.name}`} +            >                <ExclamationTriangle                  style={{ margin: "auto" }}                  width="1.0em"                  strokeWidth={2}                />              </CircleDiv> -            <StyledTooltip id={`circle-div-${props.title}`}> +            <StyledTooltip id={`circle-div-${props.item.description.name}`}>                {props.warning}              </StyledTooltip>            </> diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index bc8665fd..2cfe7ecd 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -18,9 +18,9 @@ import {  import { StopCircle } from "@styled-icons/heroicons-solid";  import { HistoryNode } from "../../../schema/HistoryNode";  import HeaderButtonWithText from "./HeaderButtonWithText"; -import MarkdownPreview from "@uiw/react-markdown-preview";  import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";  import { GUIClientContext } from "../App"; +import StyledMarkdownPreview from "./StyledMarkdownPreview";  interface StepContainerProps {    historyNode: HistoryNode; @@ -109,33 +109,6 @@ 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) { diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx new file mode 100644 index 00000000..9c2ecb62 --- /dev/null +++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx @@ -0,0 +1,37 @@ +import styled from "styled-components"; +import { +  defaultBorderRadius, +  secondaryDark, +  vscBackground, +  vscForeground, +} from "."; +import MarkdownPreview from "@uiw/react-markdown-preview"; + +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}; +`; + +export default StyledMarkdownPreview; diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx index cba3852d..7c6ba052 100644 --- a/extension/react-app/src/components/TextDialog.tsx +++ b/extension/react-app/src/components/TextDialog.tsx @@ -5,7 +5,7 @@ import { Button, secondaryDark, vscBackground, vscForeground } from ".";  import { isMetaEquivalentKeyPressed } from "../util";  const ScreenCover = styled.div` -  position: absolute; +  position: fixed;    width: 100%;    height: 100%;    background-color: rgba(168, 168, 168, 0.5); diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts index 6c0df8fc..ddf65272 100644 --- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts @@ -1,3 +1,5 @@ +import { ContextItemId } from "../../../schema/FullState"; +  abstract class AbstractContinueGUIClientProtocol {    abstract sendMainInput(input: string): void; @@ -21,15 +23,15 @@ abstract class AbstractContinueGUIClientProtocol {    abstract deleteAtIndex(index: number): void; -  abstract deleteContextAtIndices(indices: number[]): void; +  abstract deleteContextWithIds(ids: ContextItemId[]): void;    abstract setEditingAtIndices(indices: number[]): void; -  abstract setPinnedAtIndices(indices: number[]): void; -    abstract toggleAddingHighlightedCode(): void;    abstract showLogsAtIndex(index: number): void; + +  abstract selectContextItem(id: string, query: string): void;  }  export default AbstractContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index 7d6c2a71..1048e956 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -1,3 +1,4 @@ +import { ContextItemId } from "../../../schema/FullState";  import AbstractContinueGUIClientProtocol from "./AbstractContinueGUIClientProtocol";  import { Messenger, WebsocketMessenger } from "./messenger";  import { VscodeMessenger } from "./vscodeMessenger"; @@ -68,18 +69,16 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {      this.messenger.send("delete_at_index", { index });    } -  deleteContextAtIndices(indices: number[]) { -    this.messenger.send("delete_context_at_indices", { indices }); +  deleteContextWithIds(ids: ContextItemId[]) { +    this.messenger.send("delete_context_with_ids", { +      ids: ids.map((id) => `${id.provider_title}-${id.item_id}`), +    });    }    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", {});    } @@ -87,6 +86,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {    showLogsAtIndex(index: number): void {      this.messenger.send("show_logs_at_index", { index });    } + +  selectContextItem(id: string, query: string): void { +    this.messenger.send("select_context_item", { id, query }); +  }  }  export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index fccc9b4b..a1ba1c33 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -6,7 +6,7 @@ import {  } from "../components";  import Loader from "../components/Loader";  import ContinueButton from "../components/ContinueButton"; -import { FullState, HighlightedRangeContext } from "../../../schema/FullState"; +import { ContextItem, FullState } from "../../../schema/FullState";  import { useCallback, useEffect, useRef, useState, useContext } from "react";  import { History } from "../../../schema/History";  import { HistoryNode } from "../../../schema/HistoryNode"; @@ -22,12 +22,16 @@ import TextDialog from "../components/TextDialog";  import HeaderButtonWithText from "../components/HeaderButtonWithText";  import ReactSwitch from "react-switch";  import { usePostHog } from "posthog-js/react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux";  import { RootStore } from "../redux/store";  import { postVscMessage } from "../vscode";  import UserInputContainer from "../components/UserInputContainer";  import Onboarding from "../components/Onboarding";  import { isMetaEquivalentKeyPressed } from "../util"; +import { +  setBottomMessage, +  setBottomMessageCloseTimeout, +} from "../redux/slices/uiStateSlice";  const TopGUIDiv = styled.div`    overflow: hidden; @@ -78,15 +82,13 @@ function GUI(props: GUIProps) {    const [usingFastModel, setUsingFastModel] = useState(false);    const [waitingForSteps, setWaitingForSteps] = useState(false);    const [userInputQueue, setUserInputQueue] = useState<string[]>([]); -  const [highlightedRanges, setHighlightedRanges] = useState< -    HighlightedRangeContext[] -  >([]);    const [addingHighlightedCode, setAddingHighlightedCode] = useState(false); +  const [selectedContextItems, setSelectedContextItems] = useState< +    ContextItem[] +  >([]);    const [availableSlashCommands, setAvailableSlashCommands] = useState<      { name: string; description: string }[]    >([]); -  const [pinned, setPinned] = useState(false); -  const [showDataSharingInfo, setShowDataSharingInfo] = useState(false);    const [stepsOpen, setStepsOpen] = useState<boolean[]>([      true,      true, @@ -118,6 +120,11 @@ function GUI(props: GUIProps) {    const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);    const [feedbackDialogMessage, setFeedbackDialogMessage] = useState(""); +  const dispatch = useDispatch(); +  const bottomMessage = useSelector( +    (state: RootStore) => state.uiState.bottomMessage +  ); +    const topGuiDivRef = useRef<HTMLDivElement>(null);    const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>( @@ -179,7 +186,8 @@ function GUI(props: GUIProps) {        setWaitingForSteps(waitingForSteps);        setHistory(state.history); -      setHighlightedRanges(state.highlighted_ranges); +      console.log((state as any).selected_context_items); +      setSelectedContextItems(state.selected_context_items);        setUserInputQueue(state.user_input_queue);        setAddingHighlightedCode(state.adding_highlighted_code);        setAvailableSlashCommands( @@ -214,13 +222,6 @@ function GUI(props: GUIProps) {    const mainTextInputRef = useRef<HTMLInputElement>(null); -  const deleteContextItems = useCallback( -    (indices: number[]) => { -      client?.deleteContextAtIndices(indices); -    }, -    [client] -  ); -    const onMainTextInput = (event?: any) => {      if (mainTextInputRef.current) {        let input = (mainTextInputRef.current as any).inputValue; @@ -360,11 +361,7 @@ function GUI(props: GUIProps) {            }}            onInputValueChange={() => {}}            items={availableSlashCommands} -          highlightedCodeSections={highlightedRanges} -          deleteContextItems={deleteContextItems} -          onTogglePin={() => { -            setPinned((prev: boolean) => !prev); -          }} +          selectedContextItems={selectedContextItems}            onToggleAddContext={() => {              client?.toggleAddingHighlightedCode();            }} @@ -373,29 +370,30 @@ function GUI(props: GUIProps) {          <ContinueButton onClick={onMainTextInput} />        </TopGUIDiv>        <div +        onMouseEnter={() => { +          dispatch(setBottomMessageCloseTimeout(undefined)); +        }} +        onMouseLeave={() => { +          dispatch(setBottomMessage(undefined)); +        }}          style={{            position: "fixed",            bottom: "50px", +          left: "0", +          right: "0", +          margin: "16px",            backgroundColor: vscBackground,            color: vscForeground,            borderRadius: defaultBorderRadius,            padding: "16px", -          margin: "16px",            zIndex: 100,            boxShadow: `0px 0px 10px 0px ${vscForeground}`, +          maxHeight: "50vh", +          overflow: "scroll",          }} -        hidden={!showDataSharingInfo} +        hidden={!bottomMessage}        > -        By turning on this switch, you will begin collecting accepted and -        rejected suggestions in .continue/suggestions.json. This data is stored -        locally on your machine and not sent anywhere. -        <br /> -        <br /> -        <b> -          {dataSwitchChecked -            ? "👍 Data is being collected" -            : "👎 No data is being collected"} -        </b> +        {bottomMessage}        </div>        <Footer dataSwitchChecked={dataSwitchChecked}>          <div @@ -406,10 +404,25 @@ function GUI(props: GUIProps) {              alignItems: "center",            }}            onMouseEnter={() => { -            setShowDataSharingInfo(true); +            dispatch( +              setBottomMessage( +                <> +                  By turning on this switch, you will begin collecting accepted +                  and rejected suggestions in .continue/suggestions.json. This +                  data is stored locally on your machine and not sent anywhere. +                  <br /> +                  <br /> +                  <b> +                    {dataSwitchChecked +                      ? "👍 Data is being collected" +                      : "👎 No data is being collected"} +                  </b> +                </> +              ) +            );            }}            onMouseLeave={() => { -            setShowDataSharingInfo(false); +            dispatch(setBottomMessage(undefined));            }}          >            <ReactSwitch diff --git a/extension/react-app/src/redux/selectors/uiStateSelectors.ts b/extension/react-app/src/redux/selectors/uiStateSelectors.ts new file mode 100644 index 00000000..7ebc9338 --- /dev/null +++ b/extension/react-app/src/redux/selectors/uiStateSelectors.ts @@ -0,0 +1,5 @@ +import { RootStore } from "../store"; + +const selectBottomMessage = (state: RootStore) => state.uiState.bottomMessage; + +export { selectBottomMessage }; diff --git a/extension/react-app/src/redux/slices/uiStateSlice.ts b/extension/react-app/src/redux/slices/uiStateSlice.ts new file mode 100644 index 00000000..837d19e9 --- /dev/null +++ b/extension/react-app/src/redux/slices/uiStateSlice.ts @@ -0,0 +1,24 @@ +import { createSlice } from "@reduxjs/toolkit"; + +export const uiStateSlice = createSlice({ +  name: "uiState", +  initialState: { +    bottomMessage: undefined, +    bottomMessageCloseTimeout: undefined, +  }, +  reducers: { +    setBottomMessage: (state, action) => { +      state.bottomMessage = action.payload; +    }, +    setBottomMessageCloseTimeout: (state, action) => { +      if (state.bottomMessageCloseTimeout) { +        clearTimeout(state.bottomMessageCloseTimeout); +      } +      state.bottomMessageCloseTimeout = action.payload; +    }, +  }, +}); + +export const { setBottomMessage, setBottomMessageCloseTimeout } = +  uiStateSlice.actions; +export default uiStateSlice.reducer; diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts index b6eb55b3..d49513e5 100644 --- a/extension/react-app/src/redux/store.ts +++ b/extension/react-app/src/redux/store.ts @@ -3,6 +3,7 @@ import debugStateReducer from "./slices/debugContexSlice";  import chatReducer from "./slices/chatSlice";  import configReducer from "./slices/configSlice";  import miscReducer from "./slices/miscSlice"; +import uiStateReducer from "./slices/uiStateSlice";  import { RangeInFile, SerializedDebugContext } from "../../../src/client";  export interface ChatMessage { @@ -31,6 +32,10 @@ export interface RootStore {    misc: {      highlightedCode: RangeInFile | undefined;    }; +  uiState: { +    bottomMessage: JSX.Element | undefined; +    bottomMessageCloseTimeout: NodeJS.Timeout | undefined; +  };  }  const store = configureStore({ @@ -39,6 +44,7 @@ const store = configureStore({      chat: chatReducer,      config: configReducer,      misc: miscReducer, +    uiState: uiStateReducer,    },  });  | 
