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); |