diff options
Diffstat (limited to 'extension/react-app/src')
| -rw-r--r-- | extension/react-app/src/components/ComboBox.tsx | 176 | ||||
| -rw-r--r-- | extension/react-app/src/components/PillButton.tsx | 94 | ||||
| -rw-r--r-- | extension/react-app/src/components/StepContainer.tsx | 29 | ||||
| -rw-r--r-- | extension/react-app/src/components/StyledMarkdownPreview.tsx | 37 | ||||
| -rw-r--r-- | extension/react-app/src/components/TextDialog.tsx | 2 | ||||
| -rw-r--r-- | extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts | 10 | ||||
| -rw-r--r-- | extension/react-app/src/hooks/ContinueGUIClientProtocol.ts | 19 | ||||
| -rw-r--r-- | extension/react-app/src/main.tsx | 6 | ||||
| -rw-r--r-- | extension/react-app/src/pages/gui.tsx | 107 | ||||
| -rw-r--r-- | extension/react-app/src/redux/selectors/uiStateSelectors.ts | 5 | ||||
| -rw-r--r-- | extension/react-app/src/redux/slices/uiStateSlice.ts | 24 | ||||
| -rw-r--r-- | extension/react-app/src/redux/store.ts | 6 | 
12 files changed, 327 insertions, 188 deletions
| diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index bf07cb93..4a1cdbc0 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,6 +73,7 @@ const Ul = styled.ul<{    hidden: boolean;    showAbove: boolean;    ulHeightPixels: number; +  inputBoxHeight?: string;  }>`    ${(props) =>      props.showAbove @@ -104,35 +114,79 @@ const Li = styled.li<{  // #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()) @@ -145,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); @@ -184,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={() => { @@ -259,7 +291,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {        <div className="flex px-2" ref={divRef} hidden={!downshiftProps.isOpen}>          <MainTextInput            disabled={props.disabled} -          placeholder={`Ask a question, give instructions, or type '/' to see slash commands`} +          placeholder={`Ask a question, give instructions, type '/' for slash commands, or '@' to add context`}            {...getInputProps({              onChange: (e) => {                const target = e.target as HTMLTextAreaElement; @@ -269,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); @@ -283,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) @@ -296,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(); @@ -315,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]); @@ -322,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,            })}          /> @@ -345,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..548fdf9d 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: "11px", +                  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..8e3735ec 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; @@ -13,23 +15,21 @@ abstract class AbstractContinueGUIClientProtocol {      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 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..b8019664 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"; @@ -52,10 +53,6 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {      });    } -  changeDefaultModel(model: string) { -    this.messenger.send("change_default_model", { model }); -  } -    sendClear() {      this.messenger.send("clear_history", {});    } @@ -68,18 +65,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 +82,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/main.tsx b/extension/react-app/src/main.tsx index e29a7d5f..1776490c 100644 --- a/extension/react-app/src/main.tsx +++ b/extension/react-app/src/main.tsx @@ -8,13 +8,11 @@ import "./index.css";  import posthog from "posthog-js";  import { PostHogProvider } from "posthog-js/react"; +console.log("Starting React"); +  posthog.init("phc_JS6XFROuNbhJtVCEdTSYk6gl5ArRrTNMpCcguAXlSPs", {    api_host: "https://app.posthog.com",    disable_session_recording: true, -  session_recording: { -    // WARNING: Only enable this if you understand the security implications -    // recordCrossOriginIframes: true, -  } as any,  });  ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index 70031d40..5d893de9 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; @@ -64,9 +68,6 @@ function GUI(props: GUIProps) {    const vscMachineId = useSelector(      (state: RootStore) => state.config.vscMachineId    ); -  const vscMediaUrl = useSelector( -    (state: RootStore) => state.config.vscMediaUrl -  );    const [dataSwitchChecked, setDataSwitchChecked] = useState(false);    const dataSwitchOn = useSelector(      (state: RootStore) => state.config.dataSwitchOn @@ -80,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, @@ -117,10 +116,36 @@ function GUI(props: GUIProps) {      current_index: 3,    } as any); +  const vscMediaUrl = useSelector( +    (state: RootStore) => state.config.vscMediaUrl +  );    const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);    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>( @@ -152,6 +177,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); @@ -178,7 +205,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( @@ -211,15 +238,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; @@ -351,6 +369,7 @@ function GUI(props: GUIProps) {            })}          </div> +        <div ref={aboveComboBoxDivRef} />          <ComboBox            ref={mainTextInputRef}            onEnter={(e) => { @@ -360,11 +379,7 @@ function GUI(props: GUIProps) {            }}            onInputValueChange={() => {}}            items={availableSlashCommands} -          highlightedCodeSections={highlightedRanges} -          deleteContextItems={deleteContextItems} -          onTogglePin={() => { -            setPinned((prev: boolean) => !prev); -          }} +          selectedContextItems={selectedContextItems}            onToggleAddContext={() => {              client?.toggleAddingHighlightedCode();            }} @@ -373,40 +388,42 @@ 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}>          {vscMediaUrl && ( -          <a -            href="https://github.com/continuedev/continue" -            style={{ marginRight: "auto" }} -          > +          <a href="https://github.com/continuedev/continue">              <img src={`${vscMediaUrl}/continue-dev-square.png`} width="22px" />            </a>          )} -        {/* <p style={{ margin: "0", marginRight: "auto" }}>Continue</p> */} +        <p style={{ margin: "0", marginRight: "auto" }}>Continue</p>          <HeaderButtonWithText            onClick={() => {              // Show the dialog 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,    },  }); | 
