diff options
Diffstat (limited to 'extension/react-app/src')
11 files changed, 342 insertions, 182 deletions
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 1e2ca135..7d266c6e 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,17 @@ 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"; +import { setBottomMessageCloseTimeout } from "../redux/slices/uiStateSlice"; +import { useDispatch } from "react-redux"; + +const SEARCH_INDEX_NAME = "continue_context_items";  // #region styled components  const mainInputFontSize = 13; @@ -64,11 +73,12 @@ 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};    color: ${vscForeground}; @@ -79,15 +89,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<{ @@ -95,49 +99,94 @@ const Li = styled.li<{    selected: boolean;    isLastItem: boolean;  }>` -  background-color: ${vscBackground}; -  ${({ 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 dispatch = useDispatch(); +    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, +                }; +              }) +            ); +          }) +          .catch(() => { +            // Swallow errors, because this simply is not supported on Windows at the moment +          }); +        return; +      }        setItems(          props.items.filter((item) =>            item.name.toLowerCase().startsWith(inputValue.toLowerCase()) @@ -150,6 +199,18 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {      },    }); +  useEffect(() => { +    if (downshiftProps.highlightedIndex < 0) { +      downshiftProps.setHighlightedIndex(0); +    } +  }, [downshiftProps.inputValue]); + +  const divRef = React.useRef<HTMLDivElement>(null); +  const ulRef = React.useRef<HTMLUListElement>(null); +  const showAbove = () => { +    return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight; +  }; +    useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);    const [metaKeyPressed, setMetaKeyPressed] = useState(false); @@ -189,59 +250,25 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {      };    }, [inputRef.current]); -  const divRef = React.useRef<HTMLDivElement>(null); -  const ulRef = React.useRef<HTMLUListElement>(null); -  const showAbove = () => { -    return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight; -  }; -    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={() => { @@ -274,11 +301,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  target.scrollHeight,                  300                ).toString()}px`; +              setInputBoxHeight(target.style.height);                // setShowContextDropdown(target.value.endsWith("@"));              },              onFocus: (e) => {                setFocused(true); +              dispatch(setBottomMessageCloseTimeout(undefined));              },              onBlur: (e) => {                setFocused(false); @@ -288,6 +317,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) @@ -301,6 +331,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(); @@ -320,6 +351,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]); @@ -327,8 +359,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {                  setPositionInHistory((prev) =>                    Math.min(prev + 1, history.length)                  ); +                setCurrentlyInContextQuery(false);                }              }, +            onClick: () => { +              dispatch(setBottomMessageCloseTimeout(undefined)); +            },              ref: inputRef,            })}          /> @@ -350,13 +386,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 5929d06a..660ede09 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -1,8 +1,9 @@ -import { useContext, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react";  import styled from "styled-components";  import {    StyledTooltip,    defaultBorderRadius, +  lightGray,    secondaryDark,    vscBackground,    vscForeground, @@ -13,6 +14,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; @@ -68,19 +77,55 @@ 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} +            <pre> +              <code +                style={{ +                  fontSize: "10px", +                  backgroundColor: vscBackground, +                  color: vscForeground, +                  whiteSpace: "pre-wrap", +                  wordWrap: "break-word", +                }} +              > +                {props.item.content} +              </code> +            </pre> +          </> +        ) +      ); +    } else { +      dispatch( +        setBottomMessageCloseTimeout( +          setTimeout(() => { +            if (!isHovered) { +              dispatch(setBottomMessage(undefined)); +            } +          }, 2000) +        ) +      ); +    } +  }, [isHovered]); +    return (      <>        <div style={{ position: "relative" }}> @@ -89,10 +134,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", @@ -113,11 +156,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"} @@ -132,15 +178,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> @@ -148,33 +185,34 @@ const PillButton = (props: PillButtonProps) => {                  data-tooltip-id={`delete-${props.index}`}                  backgroundColor={"#cc000055"}                  onClick={() => { -                  if (props.onDelete) { -                    props.onDelete(); -                  } +                  client?.deleteContextWithIds([props.item.description.id]); +                  dispatch(setBottomMessageCloseTimeout(undefined));                  }}                >                  <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 9597b578..7d8e9920 100644 --- a/extension/react-app/src/components/TextDialog.tsx +++ b/extension/react-app/src/components/TextDialog.tsx @@ -6,7 +6,7 @@ import { isMetaEquivalentKeyPressed } from "../util";  import { ReactMarkdown } from "react-markdown/lib/react-markdown";  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 49f41dcf..03b24349 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; @@ -77,15 +81,13 @@ function GUI(props: GUIProps) {    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,29 @@ function GUI(props: GUIProps) {    const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");    const [feedbackEntryOn, setFeedbackEntryOn] = useState(true); +  const dispatch = useDispatch(); +  const bottomMessage = useSelector( +    (state: RootStore) => state.uiState.bottomMessage +  ); + +  const [displayBottomMessageOnBottom, setDisplayBottomMessageOnBottom] = +    useState<boolean>(true); +  const mainTextInputRef = useRef<HTMLInputElement>(null); + +  const aboveComboBoxDivRef = useRef<HTMLDivElement>(null); + +  useEffect(() => { +    if (!aboveComboBoxDivRef.current) return; +    if ( +      aboveComboBoxDivRef.current.getBoundingClientRect().top > +      window.innerHeight / 2 +    ) { +      setDisplayBottomMessageOnBottom(false); +    } else { +      setDisplayBottomMessageOnBottom(true); +    } +  }, [bottomMessage, aboveComboBoxDivRef.current]); +    const topGuiDivRef = useRef<HTMLDivElement>(null);    const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>( @@ -149,6 +174,8 @@ function GUI(props: GUIProps) {          history.timeline[history.current_index]?.active        ) {          client?.deleteAtIndex(history.current_index); +      } else if (e.key === "Escape") { +        dispatch(setBottomMessageCloseTimeout(undefined));        }      };      window.addEventListener("keydown", listener); @@ -175,7 +202,7 @@ function GUI(props: GUIProps) {        setWaitingForSteps(waitingForSteps);        setHistory(state.history); -      setHighlightedRanges(state.highlighted_ranges); +      setSelectedContextItems(state.selected_context_items);        setUserInputQueue(state.user_input_queue);        setAddingHighlightedCode(state.adding_highlighted_code);        setAvailableSlashCommands( @@ -208,15 +235,6 @@ function GUI(props: GUIProps) {      scrollToBottom();    }, [waitingForSteps]); -  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; @@ -348,6 +366,7 @@ function GUI(props: GUIProps) {            })}          </div> +        <div ref={aboveComboBoxDivRef} />          <ComboBox            ref={mainTextInputRef}            onEnter={(e) => { @@ -357,11 +376,7 @@ function GUI(props: GUIProps) {            }}            onInputValueChange={() => {}}            items={availableSlashCommands} -          highlightedCodeSections={highlightedRanges} -          deleteContextItems={deleteContextItems} -          onTogglePin={() => { -            setPinned((prev: boolean) => !prev); -          }} +          selectedContextItems={selectedContextItems}            onToggleAddContext={() => {              client?.toggleAddingHighlightedCode();            }} @@ -370,29 +385,34 @@ function GUI(props: GUIProps) {          <ContinueButton onClick={onMainTextInput} />        </TopGUIDiv>        <div +        onMouseEnter={() => { +          dispatch(setBottomMessageCloseTimeout(undefined)); +        }} +        onMouseLeave={(e) => { +          if (!e.buttons) { +            dispatch(setBottomMessage(undefined)); +          } +        }}          style={{            position: "fixed", -          bottom: "50px", +          bottom: displayBottomMessageOnBottom ? "50px" : undefined, +          top: displayBottomMessageOnBottom ? undefined : "50px", +          left: "0", +          right: "0", +          margin: "8px", +          marginTop: "0px",            backgroundColor: vscBackground,            color: vscForeground,            borderRadius: defaultBorderRadius, -          padding: "16px", -          margin: "16px", +          padding: "12px",            zIndex: 100, -          boxShadow: `0px 0px 10px 0px ${vscForeground}`, +          boxShadow: `0px 0px 6px 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 @@ -403,10 +423,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,    },  });  | 
