diff options
Diffstat (limited to 'extension/react-app/src/components')
5 files changed, 214 insertions, 124 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); | 
