diff options
author | Nate Sesti <sestinj@gmail.com> | 2023-07-26 00:26:02 -0700 |
---|---|---|
committer | Nate Sesti <sestinj@gmail.com> | 2023-07-26 00:26:02 -0700 |
commit | 79a2fa634e5b5d44e13fbd49facf14a4fc3745d1 (patch) | |
tree | 0e5917d1ae3fad12e4cf459ec273593d9d5267a4 /extension/react-app/src | |
parent | b759e2dbfe36b3e8873527b9736d64866da9b604 (diff) | |
parent | 2b69bf6f1fc2e06b16b718358ceed4911d6e87c3 (diff) | |
download | sncontinue-79a2fa634e5b5d44e13fbd49facf14a4fc3745d1.tar.gz sncontinue-79a2fa634e5b5d44e13fbd49facf14a4fc3745d1.tar.bz2 sncontinue-79a2fa634e5b5d44e13fbd49facf14a4fc3745d1.zip |
Merge branch 'config-py' into merge-config-py-TO-main
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, }, }); |